FlowField流场寻路,利用网格存储每个点对目标点的推力,网格上的单位根据对于推力进行移动。用于大量单位进行寻路对于同一目的地的寻路,常用于rts游戏等。
对应一张网格地图(图中黑块是不可行走区域)

生成热度图
计算所有网格对于目标点(图中红点)网格的路径距离。(每个格子的移动距离算作1)。
通过dijkstra算法遍历出每个格子的路径距离.(a *算法启发函数结果为0就是dijkstra算法。之前NavMesh寻路有说明过a *算法)
void FlowFieldScene::createHeadMap() {
unordered_map<int, float> openList;
unordered_map<int, float> closeList;
_distNode->removeAllChildren();
_dist.clear();
//FlowFieldMathHelper::mapHeight 地图高度,即地图在y轴上的格子数
for (int i = 0; i <= FlowFieldMathHelper::mapHeight; i++) {
vector<float> d;
d.resize(FlowFieldMathHelper::mapWidth + 1, 0);
_dist.push_back(d);
}
//转换实际位置到网格坐标的位置
Vec2 gridPos = FlowFieldMathHelper::getGridPos(_touchBeganPosition);
//每个网格都有个唯一id
int gridId = FlowFieldMathHelper::getGridIdByGridPos(gridPos.x, gridPos.y);
openList.emplace(gridId, 0);
while (!openList.empty()) {
pair<int, float> node = *openList.begin();
for (auto n : openList) {
if (node.second > n.second) node = n;
}
openList.erase(node.first);
closeList.insert(node);
Vec2 gridPos = FlowFieldMathHelper::getGridPosById(node.first);
_dist[gridPos.y][gridPos.x] = node.second;
//FlowFieldMathHelper::getNeighbor获取周边的格子,计算热度图四向就够
auto neighbors = FlowFieldMathHelper::getNeighbor(node.first);
for (auto neighbor : neighbors) {
//isBlock 判断是否是阻挡格
if (isBlock(neighbor.first)) continue;
if (closeList.find(neighbor.first) != closeList.end()) continue;
if (openList.find(neighbor.first) == openList.end()) {
openList.emplace(neighbor.first, neighbor.second + node.second);
}
else {
if (openList[neighbor.first] > neighbor.second + node.second) {
openList[neighbor.first] = neighbor.second + node.second;
}
}
}
}
}

生成向量图
生成热度图之后,遍历每个网格,查找他所有相邻的网格(8向),选择到目标点路径距离最小的网格(阻挡网格的路径距离无穷大),把当前网格的向量指向对应网格,最终生成矢量图
(注意,当周围有阻挡网格时,要判断不能斜向穿过阻挡格)

当矢量是左上(-1,1)左下(-1,-1)右上(1,1)右下(1,-1),判断目标格子周围的阻挡格。静止矢量斜向穿过障碍物
//静止倾斜穿过障碍物
bool FlowFieldScene::checkObliqueAngleBlock(int gridX, int gridY, int offsetX, int offsetY) {
if (offsetX * offsetY == 0) return false;
if (isBlock(gridX + offsetX, gridY) || isBlock(gridX, gridY + offsetY)) return true;
return false;
}
生成向量图
void FlowFieldScene::createVectorMap() {
_vectorNode->clear();
_vectorMap.clear();
for (int y = 0; y <= FlowFieldMathHelper::mapHeight; y++) {
for (int x = 0; x <= FlowFieldMathHelper::mapWidth; x++) {
if (_dist[y][x] == 0) continue;
Vec2 direct;
float neighborDist = -1;
for (int offsetX = -1; offsetX <= 1; offsetX++) {
for (int offsetY = -1; offsetY <= 1; offsetY++) {
int toX = x + offsetX;
int toY = y + offsetY;
if (isBlock(toX, toY)) continue;
else if (x == toX && y == toY) continue;
else if (toX < 0 || toX > FlowFieldMathHelper::mapWidth) continue;
else if (toY < 0 || toY > FlowFieldMathHelper::mapHeight) continue;
if ( neighborDist == -1 || neighborDist > _dist[toY][toX] ) {
if (checkObliqueAngleBlock(x, y, offsetX, offsetY)) continue;
neighborDist = _dist[toY][toX];
direct = Vec2(offsetX, offsetY);
}
}
}
_vectorMap.emplace(FlowFieldMathHelper::getGridIdByGridPos(x, y), direct);
}
}
}

设置转向力
根据设置的流场向量获得转向力
//_flowFieldDirect为设置的当前格子流场转向力方向
Vec2 MoveNode::flowField() {
if (_flowFieldDirect == Vec2::ZERO) return Vec2::ZERO;
Vec2 desiredVelocity = _flowFieldDirect * _dtSpeed;
Vec2 steering;
if (MoveSmooth) steering = desiredVelocity - _velocity;
else steering = desiredVelocity;
return steering;
}
void MoveNode::update(float dt)
{
findNeighbourObjs();
_dtSpeed = _speed * dt;
Vec2 steering = Vec2::ZERO;
steering += seek(_tarPos);
steering += flee();
steering += wander();
steering += pursuit();
steering += cohesion();
steering += separation();
steering += alignment();
steering += flowField();
steering = turncate(steering, _maxForce);
steering *= ( 1 / (float)_mass );
_velocity += steering;
_velocity += wallAvoid();
_velocity = turncate(_velocity, _maxSpeed * dt);
updatePos();
}
此时如果直接取当前所在网格的向量作流场力的方向,会出现两个问题
void FlowFieldScene::update(float dt) {
for (auto node : _moveNodes) {
int gridId = FlowFieldMathHelper::getGridId(node->getPosition());
Vec2 direct = _vectorMap[gridId];
node->setFlowFieldDirect(direct);
}
}
1.如果转向力所占的权重不够大,会导致物体转向不及时,并且可能插入阻挡格的情况

2.如果增大转向力所占权重,又容易导致物体直接贴着网格边缘行走,而非沿着中线行走

为了改善这种情况,根据所处当前格的不同位置,选取不同方向的三个相邻网格,加上自身网格的4个向量进行线插值

进行双线性插值
双线性插值

获取4个向量后,先把网格y值相同的向量,进行两两线性插值,再把求出的两个新向量进行线性插值即可
注意,双线性插值的话,如果目标点再阻挡格旁边,而阻挡格又没有向量(取0)的话,目标会直接穿过阻挡格

因此如果获取的网格是阻挡格,则直接取阻挡网格指向当前格的方向做插值。
这个是只有流场力的单独优化,实际项目中,不同力的权重可能不同,真正避免与阻挡物碰撞。还是要加上专门的碰撞避免处理(如阻挡物周围加力场,ORCA等)


Vec2 FlowFieldScene::bilinearInterpolation(Vec2 curPosition) {
Vec2 gridPos = FlowFieldMathHelper::getGridPos(curPosition

本文介绍了FlowField流场寻路,它利用网格存储推力实现大量单位对同一目的地的寻路,常用于rts游戏。详细阐述了生成热度图、向量图的方法,设置转向力时遇到的问题及双线性插值优化方案,还提及流场力与其他力结合的效果和相关源码。
2198

被折叠的 条评论
为什么被折叠?



