AI - FlowField(流场寻路)

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

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;
}

加入之前的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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值