标签:
接着上次的来,我们在群体算法之前把基本的个体运动解决掉。
此处的墙被抽象为一条线段,不论你的游戏使用的是一条线段作为墙面的碰撞检测,或者用一个几何形状作为墙面,几何形状我们可以看作多条线段的集合,都可以用此方法。
首先是线段类,作为基类,拥有几种几何计算的方法,便于计算平面线段的交点,不多说。
struct Seg
{
Seg(Point p1, Point p2):
_from(p1), _to(p2)
{
}
#define eps 1e-6
//static math functions
static int sgn(double x)
{
return x<-eps ? -1 : (x>eps);
}
static double Cross(const Point& p1, const Point& p2, const Point& p3, const Point& p4)
{
return (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x);
}
static double Area(const Point& p1, const Point& p2, const Point& p3)
{
return Cross(p1, p2, p1, p3);
}
static double fArea(const Point& p1, const Point& p2, const Point& p3)
{
return fabs(Area(p1, p2, p3));
}
static bool Meet(const Point& p1, const Point& p2, const Point& p3, const Point& p4)
{
return max(min(p1.x, p2.x), min(p3.x, p4.x)) <= min(max(p1.x, p2.x), max(p3.x, p4.x))
&& max(min(p1.y, p2.y), min(p3.y, p4.y)) <= min(max(p1.y, p2.y), max(p3.y, p4.y))
&& sgn(Cross(p3, p2, p3, p4) * Cross(p3, p4, p3, p1)) >= 0
&& sgn(Cross(p1, p4, p1, p2) * Cross(p1, p2, p1, p3)) >= 0;
}
static Point Inter(const Point& p1, const Point& p2, const Point& p3, const Point& p4)
{
double s1 = fArea(p1, p2, p3), s2 = fArea(p1, p2, p4);
return Point((p4.x*s1 + p3.x*s2) / (s1 + s2), (p4.y*s1 + p3.y*s2) / (s1 + s2));
}
static double PointToSegDist(double x, double y, double x1, double y1, double x2, double y2)
{
double cross = (x2 - x1) * (x - x1) + (y2 - y1) * (y - y1);
if (cross <= 0) return sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
double d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
if (cross >= d2) return sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
double r = cross / d2;
double px = x1 + (x2 - x1) * r;
double py = y1 + (y2 - y1) * r;
return sqrt((x - px) * (x - px) + (py - y1) * (py - y1));
}
Point _from;
Point _to;
};
墙类保护继承于线段类,我们并不把Seg类的几何计算方法暴露出来。
class Wall :public BaseEntity,protected Seg//inherite math functions as protected functions
{
public:
Wall(Vec2, Vec2);
virtual ~Wall();
public:
virtual void update();
virtual bool handleMsg(const Telegram&);
public:
Vec2 from()const { return _from; }
Vec2 to()const { return _to; }
Vec2 normal()const
{
Vec2 normalLine(_to.x - _from.x, _from.x - _to.x);
return (normalLine.getNormalized());
}
static bool lineIntersection(const Vec2 entPo, const Vec2 fleer, const Wall wall, double& entity2wall, Vec2& intersection);
};
bool Wall::lineIntersection(const Vec2 entPo, const Vec2 feeler, const Wall wall, double& entity2wall, Vec2& intersection)
{
if (!Seg::Meet(entPo, feeler, wall.from(), wall.to()))
{
return false;
}
else
{
entity2wall = Seg::PointToSegDist(entPo.x, entPo.y, wall.from().x, wall.from().y, wall.to().x, wall.to().y);
intersection = Seg::Inter(entPo, feeler, wall.from(), wall.to());
return true;
}
}
类似于上一篇,这次我们用“触须”来作为辅助图形
在猫科动物运动过程中,触须会帮助它们判断前方物体是否会碰撞,例如洞口能否钻过,此处仿真了这一点。
一般我们使用三根触须,触须从实体坐标出发,方向分别为沿朝向,然后两个45°角,沿朝向的长度为两侧的两倍。
触须会提前插入墙壁,产生的斥力作用于交点,斥力大小与插入深度成正比。
触须长度满足这样的规则:
类型一:永不碰撞规则
物体以最大速度运动,垂直于墙壁时,从触须接触墙壁开始,产生的斥力刚好让其在墙壁前停止运动。
m·v=∫f·dt 冲量的积分刚好等于其动量,此处f表示斥力的计算公式,在此处,f=插入墙壁深度。
类型二:允许碰撞
如果允许碰撞,表示你设定的最大速度超过了“安全速度”,我们需要使用基本刚体碰撞来避免其重叠(这步在游戏逻辑层的最后实现)。
我们先来创建触须,添加成员变量:
std::vector<Vec2> _feelers;
double _wallDetectionFeelerLength;
void SteeringBehaviors::createFeelers()
{
//feeler pointing straight in front
_feelers[0] = _ownerVehicle->position() + _wallDetectionFeelerLength * _ownerVehicle->heading();
//feeler to left
Vec2 temp = _ownerVehicle->heading();
//Vec2DRotateAroundOrigin(temp, halfPI * 3.5f);
temp.rotate(Vec2::ZERO, halfPI*3.5f);
_feelers[1] = _ownerVehicle->position() + _wallDetectionFeelerLength / 2.0f * temp;
//feeler to right
temp = _ownerVehicle->heading();
temp.rotate(Vec2::ZERO, halfPI * 0.5f);
_feelers[2] = _ownerVehicle->position() + _wallDetectionFeelerLength / 2.0f * temp;
}
其实就是设定好大小和角度而已。
紧接着我们开始计算_wallDetectionFeelerLength
在总时间t内,动量等于冲量的积分:
m.Vmax=∫t0f(t)dt(1)
在任意时刻t0 ,速度等于初始速度减加速度的积分:Vt0=Vmax?∫t00f(t)/mdt(2)
作用力的表达式(此处定义一个值来配平单位:Da=1m/s3 ):f(t)=dis=∫t00Vt0dt(3)
长度的表达式:L=∫t0Vt0dt(4)
解出触须长度的表达式即可。
然后我们根据想要实现的效果
(1)允许碰撞:
(2)不允许碰撞:
说了很多题外话,其实我们游戏开发中远没有那么精确,大部分是经验值,但往往经验值效果会很好。
我们继续来看控制力的计算,我们接下来遍历三条触须与墙面的交点,找出离物体最近的墙面与触须的交点,得出斥力:
Vec2 SteeringBehaviors::wallAvoidance(const std::vector<Wall>& walls)
{
createFeelers();
//temporary vars
double dis2wall = 0.0;
double dis2Closestwall = max_double;
int closestWall = -1;
Vec2 steeringForce = Vec2::ZERO;
Vec2 point = Vec2::ZERO;
Vec2 closestPoint = Vec2::ZERO;
for (int i = 0; i < _feelers.size(); i++)
{
for (int j = 0; j < walls.size(); j++)
{
if (Wall::lineIntersection(_ownerVehicle->position(), _feelers.at(i), walls.at(j), dis2wall, point))
{
if (dis2wall < dis2Closestwall)
{
dis2Closestwall = dis2wall;
closestWall = j;
closestPoint = point;
}
}
}//next wall
if (closestWall != -1)
{
Vec2 over = _feelers.at(i);
steeringForce = walls.at(closestWall).normal()*over.getLength();
}
}//next feelers
return steeringForce;
}
物体为了躲避危险,会藏到危险物看不见的地方,就像躲猫猫一样,此处的障碍体我们暂且设定为圆形,以后可以添加墙面等作为优化。
如图,我们找出躲藏点hiding:方向=障碍物圆心 - danger的position,位置=障碍物半径 + 定义的固定长度:
#define disFromBoundary 30.0
Vec2 SteeringBehaviors::getHidePosition(const BaseEntity* obstacle,const Vec2 target)
{
double dis = obstacle->getBoundingRadius() + disFromBoundary;
Vec2 target2Obstacle = (obstacle->position() - target).getNormalized();
return (dis*target2Obstacle + obstacle->position());
}
如果障碍物是由墙面构成的几何体,我们重载该方法,将障碍物的圆心替换为几何体中心,其余基本相同,但是arrive方法要考虑到避免碰撞,需要写一个新的组合方法,实际遇到该问题时再来重写。
我们遍历距离物体一定范围内的所有障碍物,找出所有hiding,找出最近的点,靠近它:
Vec2 SteeringBehaviors::hide(const Vehicle* target, const std::vector<BaseEntity*> obstacles)
{
double disSq2Closest = max_double;
Vec2 closestHidingSpot = Vec2::ZERO;
for_each(obstacles.cbegin(), obstacles.cend(), [this,target,&disSq2Closest,&closestHidingSpot](BaseEntity* eachObstacle)
{
Vec2 hidingSpot = getHidePosition(eachObstacle, target->position());
double disSq = (hidingSpot - _ownerVehicle->position()).getLengthSq();
if (disSq < disSq2Closest)
{
disSq2Closest = disSq;
closestHidingSpot = hidingSpot;
}
}
);
//end for_each
if (disSq2Closest == max_double)
{
return evade(target);
}
return arrive(closestHidingSpot, fast);
}
我们可以加上视觉元素让物体不那么“聪明”(这样会很笨拙),物体只有在看到danger时才会做出hide,只需要在hide方法最开头加一段:
bool isHide = false;
for_each(obstacles.cbegin(), obstacles.cend(), [this,target,&isHide](BaseEntity* obs){
if (Seg::PointToSegDist(obs->position(), Seg(_ownerVehicle->position(), target->position())) < obs->getBoundingRadius())
isHide = true;
});
if (isHide)
return Vec2::ZERO;
if (Vec2(_ownerVehicle->position(), target->position()).getLengthSq < _sightDis*_sightDis)
return Vec2::ZERO;
玩过dota,war3,星际等游戏的童鞋应该知道shift操作就是按照你的指定路线操作,此处相同,我们规定path为多条首尾相接的直线的集合,物体会沿着path做循环或不循环的运动。
typedef std::function<void(Vec2,Vec2)> PointOperater;
class Path
{
private:
std::list<Vec2> _pointsOfPath;
std::list<Vec2>::iterator _currentPoint;
bool _isLooped;
public:
#define two_PI 6.283185
Path(bool isLooped):
_isLooped(isLooped)
{
}
Path(int numberOfPoints, Rect limit, bool isLooped):
_isLooped(isLooped)
{
}
virtual ~Path()
{
}
Path(const Path& other)
{
assert(this != &other&&"same");
this->set(other);
}
Path& operator=(const Path& other)
{
assert(this != &other&&"same");
this->set(other);
return *this;
}
public:
Vec2 currentPoint()const { return *_currentPoint; }
bool isFinished()const { return _currentPoint == _pointsOfPath.end(); }
void loopOn(){ this->_isLooped = true; }
void loopOff(){ this->_isLooped = false; }
bool isLooped()const { return _isLooped; }
//methods for setting the path with either another Path or a list of vectors
void set(std::list<Vec2> new_path){ _pointsOfPath = new_path; _currentPoint = _pointsOfPath.begin(); }
void set(const Path& path){ _pointsOfPath = path.getPath(); _currentPoint = _pointsOfPath.begin(); }
void clear(){ _pointsOfPath.clear(); }
std::list<Vec2> getPath()const{ return _pointsOfPath; }
public:
void addWayPoint(Vec2 new_point);
void moveToNextPoint();
void createRandomPath(int, Rect);
void pathOperate(PointOperater);//对两个相邻点做回调操作,可以是纹理,动画等
};
void Path::moveToNextPoint()
{
assert(this->_pointsOfPath.size() != 0 && "NULL");
if (++_currentPoint == _pointsOfPath.end())
{
if (_isLooped)
{
_currentPoint = _pointsOfPath.begin();
}
}
}
void Path::addWayPoint(Vec2 point)
{
this->_pointsOfPath.push_back(point);
}
void Path::createRandomPath(int num,Rect limit)
{
double midX = (limit.getMaxX() + limit.getMinX()) / 2;
double midY = (limit.getMaxY() + limit.getMinY()) / 2;
double smallerRadius = std::min(midX, midY);
double largerRadius = std::max(midX, midY);
double dDegree = two_PI / num;
for (int i = 0; i < num; i++)
{
Vec2 point = Vec2::ZERO;
while (limit.containsPoint(point) && point != Vec2::ZERO)
{
double randomDegree = Random::Rand((i)*dDegree, (i + 1)*dDegree);
double randomDis = Random::Rand(0, largerRadius);
point = (randomDis, 0);
point.rotateByAngle(Vec2(midX, midY), randomDegree);
}
_pointsOfPath.push_back(point);
}
}
void Path::pathOperate(PointOperater func)
{
std::list<Vec2>::const_iterator iter = _pointsOfPath.cbegin();
Vec2 point = *iter++;
while (iter != _pointsOfPath.end())
{
func(point,*iter);
point = *iter++;
}
if (_isLooped)
{
func(*(--iter), *_pointsOfPath.cbegin());
}
}
path类比较简单,只有一个createRandomPath比较麻烦,此处做了近似处理,只有测试的时候可能用一用,所以不用管它。
我为path的遍历添加了回调函数,我们可以其中执行cocos的render渲染等操作。
路径跟随就很简单了,找到最近的路径点,然后沿着点运动
Vec2 SteeringBehaviors::pathFollowing()
{
//ignore evaluting if it‘ on path
if (_currentPath->currentPoint().distance(_ownerVehicle->position()) < _pathPointSeekDis*_pathPointSeekDis);
{
_currentPath->moveToNextPoint();
}
if (!_currentPath->isFinished())
{
return seek(_currentPath->currentPoint());
}
else
{
return arrive(_currentPath->currentPoint(), normal);
}
}
准备写一个有关游戏底层算法,物理算法,以及AI(重点是机器学习在游戏中的应用)的长篇博客,欢迎大家指正交流╰( ̄▽ ̄)╯
标签:
原文地址:http://blog.csdn.net/qq_22984991/article/details/51189519