3、实现可移动的游戏智能体
自治智能体:
一个位于环境内部,能够感知环境对它有效实施的作用,并按此进行,为未来的新感知提供条件。比如兔子正在吃草,看见狐狸,就自治的逃走,狐狸会自治的追捕
要讨论操控行为之前,首先要对模型(运动)进行类设计,如下图所示:
BaseGameEntity是角色的基类(该基类就有Update方法),它包含了角色id,位置,大小等信息,MovingEntity继承于它代表它是一个可移动的角色,包含了重量,速度,最大速度,最大作用力,最大转向角度等信息,而Vehicle继承于MovingEntity,在private中获取GameWorld指针来获得环境中的各种物体(草地、建筑物),拿到行为类SteeringBehaviors的指针,在Update中调用该指针进行相应的行为动作,在SteeringBehaviors包含多种行为函数(寻找,到达等等)
在Vehicle的Update函数中这样定义:
bool Vehicle::Update(double time_elapsed)
{
//计算操控行为的合力
SVector2D SteeringForce = m_pSteering->Calculate();
//加速度=力/质量
SVector2D acceleration = SteeringForce / m_dMass;
//根据牛顿定律,速度=初速度+加速度*时间
m_vVelocity += acceleration * time_elapsed;
//更新位置
m_vpos += m_vVelocity * time_elapsed;
//如果速度大于一个很小的值,更新朝向
if (m_vVelocity.LengthSq() > 0.000001)
{
m_vHeading = Vec2DNormalize(m_vVelocity);
m_vSide = m_vHeading.Perp();
}
}
因为移动体朝向和速度一致,所以需要更新,让它等于速度的单位向量,但是移动体的速度大于一个很小的值时,才能计算出朝向,这是因为如果速度为0,程序会出现除0错误,如果速度不为0但是很小,移动体停下来后还会移动一小段时间
要实现一个物体的靠近一个物体的函数,首先要确定预期的速度,如下图可见:
预期速度是角色到目标的向量,大小为角色的最大速度,我们需要对角色施加一个速度,这个速度为预期速度-当前速度,施加这个速度后,物体就会朝向预期速度方向移动
因此Seek函数这样写:
//该函数传入目标位置
Vector2D SteeringBehaviors::seek(Vector2D TargetPos)
{
//取得预期速度的单位向量*角色最大速度=角色朝着预期方向的速度
Vector2D DesiredVelocity = Vec2DNormalize(TargetPos - m_pVehicle->Pos()) * m_pVehicle->MaxSpeed();
return (DesiredVelocity - m_pVehicle->Velocity());
}
对于实现角色徘徊、随机走动的函数:
一个幼稚的做法事每帧都计算出一个随机的力,但这会产生抖动,不能达到持久的转弯,当然也可以用一个随机函数(Perlin噪声)可以产生光滑转弯,但是CPU开销很大
检测是否有障碍函数:
如下图,三角为角色,圆形是障碍物,要实现避开障碍首先要检测面前是否有障碍物,给角色前方加一个矩形的长条叫检测盒,检测盒的宽度是角色宽度,长度正比于角色当前移动速度。移动越快,矩形越长。
算法的思想是这样的:
上图是世界空间中,将角色的中心点为原点,面向为x轴,垂直为y轴,建立局部坐标系,如下图,将障碍物转换到局部坐标系下。检测障碍物的半径+检测盒宽度的一半是否小于障碍物局部空间坐标下的y坐标,小于说明角色会碰到。
检测完后还要判断当前障碍物是否是距离角色最近的障碍物。计算相交点是不是距离角色最近的那个点。R=障碍物半径+检测盒宽度的一半,yA是障碍物的y坐标,勾股定理算出x轴方向上的距离,再加上障碍物的x轴坐标即可。
如果判断到角色即将撞到障碍物,需要给角色侧面产生一个偏移力让角色远离障碍物,该力如下图计算,最后还要记得把该力的方向从局部空间转到世界空间。同时要给角色在x轴方向一个制动力,该力和角色和障碍物的距离成正比。
结果:在角色的y轴方向给一个侧面偏移力,为了让角色移动,产生一个x轴方向的制动力。
群集行为:
操控行为通常用来为电影生成特别的效果,比如群居的企鹅群的运动,兽人军队的移动等。
算法思想:
最初提出的Flocking只是包含了separation、alignment和cohesion三个组合,但是为了让其能运动,还要加上wander函数。
Separation(分离)函数:产生一个力,选中主角半径内的所有成员,让力的方向为成员和主角的方向,大小除以成员和主角的距离。这样成员会远离主角。
Alignment(队列)函数:计算一个方向,为主角的面向,主角半径内的所有成员的方向为主角的方向
Cohesion(聚集)函数:产生一个力,选中主角半径内的所有成员,计算主角位置,让成员移动到主角身边。
可以将操控行为都放在类SteeringBehaviors的方法中,通过存取方法把不同行为的开关开启或关闭,从而激活或注销该行为。比如一只羊可以这样设置
Vehicle* Sheep = new Vehicle();
Sheep->Steering()->SeparationOn();
Sheep->Steering()->WanderOn();
想要实现多个汽车,从a跑到b,既要和前面的跟上前面的警车(flocking函数),同时要避开阻止它的机器人和墙(wall avoidance函数),因此要实现游戏中角色的运动是多个行为函数的叠加。有如下方式:
第一种:加权截断总和
给每一种操控行为乘以一个权值,再加一起,然后把结果截断到可允许的最大操控力。
缺点:每一个被激活的行为每帧都要进行计算。销毁cpu。其次,两个操控力相冲突。
第二种:带优先级的加权截断累计
可以在cpu运行速度和精确度之间有一个很好的折中,wall avoidance和obstacle avoidance函数行为的优先级更高,角色不能和墙、障碍物相交。
除了优先级,迭代每个被激活的行为,在计算每个新行为后,计算力的总和,看可用的操控力是多少,如果有剩余额(一个规定的力的上限,超过就不再计算更低优先级的力了),新的力被加到累计中。如果没有剩余的力,返回false,不考虑进一步行为。
脚本语言的优势:
很大的项目,编译源码的时间很长,如果只是改变几个常量需要重新编译,以前常见的方法是把常量放在一个单独的初始化文件中,写一段代码去读取并解析该文件,这样就不用重新编译了,只需更改初始化文件和配置文件的值即可,这个初始化文件就是脚本的一个初级形式。
另外脚本的优美之处在于虚拟机和c++进行通信,数据可以来回传递。
脚本语言的分类:
1、解释执行的脚本通过解释器,脚本被逐行读取解析并执行,存在被玩家轻松理解并编辑的可能。
2、编译执行的脚本是已经被脚本语言编译器编译为某种形式的机器语言的脚本,这种机器语言代码虚拟机是可以直接执行的。这种机器码编译为虚拟机执行的代码,编译的脚本执行速度更快,容量更小,字节码另一个好处是人类无法读懂,保证了脚本不容易被用户滥用。