AI 编程实践
自治的可移动游戏智能体
SteeringBehaviors(转向行为)
这些方法都返回执行该行为需要的力(实际是返回单位质量的物体单位时间内修正速度需要的加速度,根据F=m(v1-v0)/t, 这个返回值也是单位质量的物体具有该加速度需要的力)。
Seek (靠近某个位置)
//返回一个能让物体朝向目标移动的力
Vector2D SteeringBehavior::Seek(Vector2D TargetPos)
{
//由目标距离和当前距离算出“期望速度”,大小是最大速度
Vector2D DesiredVelocity = Vec2DNormalize(TargetPos - m_pVehicle->Pos())
* m_pVehicle->MaxSpeed();
//期望速度 - 当前速度 = 加速度
return (DesiredVelocity - m_pVehicle->Velocity());
}
Flee (离开某个位置)
或者叫远离某个位置。
Vector2D SteeringBehavior::Flee(Vector2D TargetPos)
{
/* PanicDistanceSq 可以设定逃逸距离,在这个距离内,才发生逃逸。
if (Vec2DDistanceSq(m_pVehicle->Pos(), target) > PanicDistanceSq)
{
return Vector2D(0,0);
}
*/
//Flee和和Seek一样,只是方向正好相反
Vector2D DesiredVelocity = Vec2DNormalize(m_pVehicle->Pos() - TargetPos)
* m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}
Arrive (抵达某个位置)
Seek行为无法停在目标处,而是不断在目标位置穿来穿去。Arrive可以让物体慢慢停在目标位置。
/**
*@param:deceleration {enum Deceleration{slow = 3, normal = 2, fast = 1};}
*/
Vector2D SteeringBehavior::Arrive(Vector2D TargetPos,
Deceleration deceleration)
{
Vector2D ToTarget = TargetPos - m_pVehicle->Pos();
//当前位置到目标位置距离
double dist = ToTarget.Length();
if (dist > 0)
{
//根据这个Scalar和Deceleration计算到达目标位置的期望速度(不需要精确)。
const double DecelerationTweaker = 0.3;
//speed是希望物体到达目标时的速度大小
double speed = dist / ((double)deceleration * DecelerationTweaker);
//确保速度不超过最大速度
speed = min(speed, m_pVehicle->MaxSpeed());
// ToTarget / dist * speed更好理解, 向量/大小就是normal的过程
Vector2D DesiredVelocity = ToTarget * speed / dist;
return (DesiredVelocity - m_pVehicle->Velocity());
}
return Vector2D(0,0);
}
Pursuit (追逐、拦截)
追逐(其实叫拦截更恰当)是预测目标的运动轨迹,然后往预测位置走。预测的难点在于预测目标走多远两者碰头。
很明显,这个“多远”正比于两者的距离,反比于两者的速度。当知道了“多远”,就能算出追逐者seek的位置。
/**
*@param: Evader {Vehicle} 逃避者
*/
Vector2D SteeringBehavior::Pursuit(const Vehicle* Evader)
{
//当前位置的偏移向量
Vector2D ToEvader = Evader->Pos() - m_pVehicle->Pos();
//和目标运动方向的趋近程度(1是同方向,-1是反方向)
double RelativeHeading = m_pVehicle->Heading().Dot(evader->Heading());
if(ToEvader.Dot(m_pVehicle->Heading()) > 0 && RelativeHeading < -0.95) //acos(0.95)=18°, (< -0.95)表示和目标运动反方向的夹角在18°以内,基本面对面
{
//直接seek目标位置
return Seek(Evader->Pos());
}
//不是面对面的话,需要预测目标继续走多久,我们能追上
//计算碰头的时间,这是个经验公式(正比于两者的距离,反比于两者的速度),为了平衡性能和准确度
double LookAheadTime = ToEvader.Length() / (m_pVehicle->MaxSpeed() + evader->Speed());
//为了给AI转向目标位置的时间,可以适当增加这个预计的碰头时间
LookAheadTime += TurnAroundTime(m_pVehicle, evader->Pos());
Vector2D FuturePos = evader->Pos() + evader->Velocity() * LookAheadTime ;
return Seek(FuturePos);
}
/**
* 计算原地转向的时间
*/
double TurnAroundTime(const Vehicle* pAgent, Vector2D TargePos)
{
//确定到目标的标准向量
Vector2D ToTarget = Vec2DNormalize(TargePos - pAgent->Pos());
double dot = pAgent->Heading().Dot(ToTarget);
//设定一个系数,pAgent的最大转弯率越高,这个值越小。(原文“这个值越大”有误,m_dMaxTurnRate是每秒最大旋转弧度,显然应该成反比)
//这个系数也正比于[转动角位移]
//如果pAgent正朝向到目标位置的反方向,那么0.5意味着这个函数返回1秒的时间让pAgent转弯
const double coefficient = 0.5;
//如果目标在正前方,那么dot=1,返回0秒
//如果目标在正后方,那么dot=-1,返回1秒
return (dot - 1.0) * -coefficient; //依旧是个经验公式
}
Evade (逃避)
Evade是Pursuit的相反行为。
/**
* 和pursuit一致,除了AI体远离追赶者预计的位置
*/
Vector2D SteeringBehavior::Evade(const Vehicle* pursuer)
{
//这次没有必要检查是否面对面了
Vector2D ToPursuer = pursuer->Pos() - m_pVehicle->Pos();
//在追赶者的威胁范围内,才逃避
const double ThreatRange = 100.0f;
if(ToPursuer.LengthSq() > ThreatRange * ThreatRange)
return Vecter2D(/**0*/);
//计算碰头的时间,这个和pursuit一致
double LookAheadTime = ToEvader.Length() / (m_pVehicle->MaxSpeed() + evader->Speed());
//这里使用Flee远离这个位置
Vector2D FuturePos = pursuer->Pos() + pursuer->Velocity() * LookAheadTime ;
return Flee(FuturePos);
}
Wander (徘徊)
Wander的做法是在Agent前方设置一个圆,圆上面找一个点,这个点每一帧随机偏移一个距离,然后新位置投射回圆上作为点的下一个位置。
Vector2D SteeringBehavior::Wander()
{
//Wander行为依赖于帧率
//m_dWanderJitter 每秒加到目标点的抖动位移
double JitterThisTimeSlice = m_dWanderJitter * m_pVehicle->TimeElapsed();
//随机偏移 RandomClamped() => RandFloat() - RandFloat(),返回(-1, 1)
m_vWanderTarget += Vector2D(RandomClamped() * JitterThisTimeSlice,
RandomClamped() * JitterThisTimeSlice);
//把目标点投射回圆上面
m_vWanderTarget.Normalize();
m_vWanderTarget *= m_dWanderRadius;
//m_dWanderDistance wander圈突出在Agent前面的距离
//这里agent本地坐标系的前边是X轴的方向
Vector2D target = m_vWanderTarget + Vector2D(m_dWanderDistance, 0);
//把target转换到世界空间
Vector2D TargetWorld = PointToWorldSpace(target,
m_pVehicle->Heading(),
m_pVehicle->Side(),
m_pVehicle->Pos());
//这里返回的是转向力,是的这里不再是DisiredVolocity - Volocity,在Craig Reynolds的论文里,TargetWorld - m_pVehicle->Pos()就是转向力
return TargetWorld - m_pVehicle->Pos();
}
Obstacle Avoidance (障碍躲避)
使用圆(3D用球)来描述障碍物,使用长方形盒(3D用圆柱)来描述操控的物体,盒子的宽等于物体的包围半径,长正比于速度。
算法总体分2部分:首先寻找要躲避的障碍物,然后计算躲避的转向力。
寻找要躲避的障碍物
详细步骤如下:
- 寻找并标记在物体检测范围内的障碍物。(检测范围:2者的距离小于长方形盒的长加障碍物半径的和)
- 将标记的障碍物换算到目标的本地空间,这一步将去掉目标后边的物体(换算后X坐标小于0)。
- 扩大障碍物半径,找出和物体重叠的障碍物。(障碍物换算后的Y坐标绝对值,是否小于它的半径+操控物体AABB宽的一半)
- 这里只剩下和物体相交的障碍物了。接下来我们寻找离物体最近的相交点。通过简单的线-圆交点算法求出相交的点(交点的Y=0,直接通过圆方程求X),然后过滤出有最小正交点的障碍物。
Vector2D SteeringBehavior::ObstacleAvoidance(const std::vector<BaseGameEntity*>& obstacles)
{
/**查找要躲避的障碍物*/
//检测用的AABB,长度与速度正相关
m_dDBoxLength = Prm.MinDetectionBoxLength * (1 + m_pVehicle->Speed() / m_pVehicle->MaxSpeed());
//如果障碍物的圆在以物体AABB的长度为半径的圆里,标记这个障碍物
m_pVehicle->World()->TagObstaclesWithinViewRange(m_pVehicle, m_dDBoxLength);
//保存最近的相交的障碍物
BaseGameEntity* ClosestIntersectionObstacle = NULL;
//保存到最近的相交的障碍物的距离
double DistToClosestIP = MaxDouble;
//保存物体本地坐标下最近的相交的的障碍物坐标
Vector2D LocalPosOfClosestObstacle;
std::vector<BaseGameEntity*>::const_iterator curOb = obstacles.cbegin();
while(curOb != obstacles.cend())
{
if((*curOb)->IsTagged())
{
//将障碍物的坐标转到物体的本地坐标系
Vector2D LocalPos = PointToLocalSpace((*curOb)->Pos(),
m_pVehicle->Heading(),
m_pVehicle->Side(),
m_pVehicle->Pos());
//过滤掉X为负值的障碍物,因为它在物体后边
if(LocalPos.x >= 0)
{
//如果障碍物到X轴的距离(Y的绝对值)小于它的半径加物体AABB宽度一半(物体半径),那么它就有可能是我们要查找的障碍物
double ExpandedRadius = (*curOb)->BRadius() + m_pVehicle->BRadius();
if(fabs(LocalPos.y) < ExpandedRadius)
{
//现在做线(物体的X轴)圆(障碍物,半径为ExpandedRadius)的相交测试,我们需要障碍物与X轴的交点全部在正半轴。
//cX,cY为圆心,根据圆的方程,与X轴的交点坐标为: x = cX +/- Sqrt(r^2 - (y-cY)^2 ), y = 0
double cX = LocalPos.x, cY = LocalPos.y;
double SqrtPart = sqrt(ExpandedRadius * ExpandedRadius - cY * cY);
//只需要计算x的最小正值
double ip = cX - SqrtPart;
if(ip <= 0.0)
{
ip = cX + SqrtPart;
}
//记录最近的障碍物数据
if(ip < DistToClosestIP)
{
DistToClosestIP = ip;
ClosestIntersectionObstacle = *curOb;
LocalPosOfClosestObstacle = LocalPos;
}
}
}
}
curOb ++;
}
}
计算躲避的转向力
操控力通常分为2部分:侧向操控力和制动操控力。
{
...
/**第二步计算转向力*/
Vector2D SteeringForce;
if(ClosestIntersectionObstacle)
{
//越接近障碍物,转向力应该越大
double multiplier = 1.0 + (m_dDBoxLength - LocalPosOfClosestObstacle.x) / m_dDBoxLength;
//计算侧向力
SteeringForce.y = (ClosestIntersectionObstacle->BRadius() - LocalPosOfClosestObstacle.y) * multiplier;
//施加一个制动力(运动反方向),越接近障碍物,力越大
const double BrakingWeight = 0.2;
SteeringForce.x = (ClosestIntersectionObstacle->BRadius() - LocalPosOfClosestObstacle.x) * BrakingWeight;
}
//最后把转向力转换到世界空间
return VectorToWorldSpace(SteeringForce,
m_pVehicle->Heading(),
m_pVehicle->Side());
}