流场寻路(Flow Field Path Finding)

简介

当场景中有成千上万个寻路游戏单位需要到达同一目标点时,通过常用的A*算法进行寻路不再是合适的选择,因为每个寻路游戏单位都需要依据自身所在的位置,根据算法获得一条从自身位置寻路到目标点的路径,n个游戏单位进行寻路就需要执行n次算法,这是比较大的性能开销。而流场寻路的方式便可以很好的解决这一问题,通常用于RTS(即时战略)游戏中的群体寻路。

将场景分割为x * y个网格节点,当确认目标点时,流场寻路算法会依据目标点所在的网格,依次向周围获取邻节点,计算邻节点到当前节点的代价。代价大的节点指向代价小的节点便可以形成一个方向,而这些节点形成的方向,便是流场,如下图所示。

流场形成后,寻路游戏单位只需要根据自身所在的坐标位置,获得对应的网格节点,依据节点的方向进行移动最终便可以到达目标点。

网格节点

节点类除了节点索引对应的字段x、y之外,还包括表示代价的字段cost与fCost,以及形成流场的节点方向字段。cost为基础代价,场景中可能会有多种地形,例如水泥地、沼泽地等等,不同地形有不同的代价,本文暂不考虑不同地形的情况,假设所有节点所在的地形是相同的,只考虑节点是否为可行走区域,通过字段isWalkable表示,而fCost是指节点到目标节点的最终代价。

using UnityEngine;

public class FlowFieldPathFinding_GridNode
{
    public int x;
    public int y;
    public int cost;
    public int fCost;
    public Vector3 direction;
    public bool isWalkable;

    public FlowFieldPathFinding_GridNode(int x, int y, bool isWalkable)
    {
        this.x = x;
        this.y = y;
        this.isWalkable = isWalkable;
        cost = isWalkable ? 10 : int.MaxValue;
        fCost = int.MaxValue;
    }
}

x * y个网格节点形成最终的网格,将所有的网格节点存储到一个字典中,通过索引i * x + j获取对应的网格节点,i表示第几行,j表示第几列,假设通过Texture资产存储地图数据,字节为255表示为可行走区域,否则为障碍区域。

public class FlowFieldPathFinding_Grid
{
    private readonly int x;
    private readonly int y;
    private readonly Dictionary<int, FlowFieldPathFinding_GridNode> nodesDic
        = new Dictionary<int, FlowFieldPathFinding_GridNode>();

    public FlowFieldPathFinding_Grid(int x, int y, bool[,] map)
    {
        this.x = x;
        this.y = y;
        for (int i = 0; i < x; i++)
        {
            for (int j = 0; j < y; j++)
            {
                int index = i * x + j;
                nodesDic.Add(index, new FlowFieldPathFinding_GridNode(i, j, map[i, j]));
            }
        }
    }
    public FlowFieldPathFinding_Grid(int x, int y, Texture2D map)
    {
        this.x = x;
        this.y = y;
        byte[] bytes = map.GetRawTextureData();
        if (bytes.Length != x * y)
            throw new ArgumentOutOfRangeException();
        for (int i = 0; i < x; i++)
        {
            for (int j = 0; j < y; j++)
            {
                int index = i * x + j;
                nodesDic.Add(index, new FlowFieldPathFinding_GridNode(i, j, bytes[index] == 255));
            }
        }
    }
}

算法思路

网格类需要提供设置目标节点的方法,设置目标节点时,将其放入一个队列中,当队列的数量大于0时,不断执行以下步骤:

  • 出列,获得当前节点;
  • 获取当前节点的邻节点;
  • 遍历邻节点,计算邻节点到当前节点的基础代价;
  • 邻节点到当前节点的基础代价加上当前节点的最终代价记为t,如果t小于邻节点的最终代价,设置邻节点的最终代价为t,并将其放入队列。
/// <summary>
/// 设置目标节点
/// </summary>
/// <param name="target"></param>
public void SetTarget(FlowFieldPathFinding_GridNode target)
{
    foreach (var node in nodesDic.Values)
    {
        node.cost = node.isWalkable ? 10 : int.MaxValue;
        node.fCost = int.MaxValue;
    }
    target.cost = 0;
    target.fCost = 0;
    target.direction = Vector3.zero;
    Queue<FlowFieldPathFinding_GridNode> queue
        = new Queue<FlowFieldPathFinding_GridNode>();
    queue.Enqueue(target);
    while (queue.Count > 0)
    {
        FlowFieldPathFinding_GridNode currentNode = queue.Dequeue();
        List<FlowFieldPathFinding_GridNode> neighbourNodes = GetNeighbouringNodes(currentNode);
        for (int i = 0; i < neighbourNodes.Count; i++)
        {
            FlowFieldPathFinding_GridNode neighbourNode = neighbourNodes[i];
            if (neighbourNode.cost == int.MaxValue) continue;
            neighbourNode.cost = CalculateCost(neighbourNode, currentNode);
            if (neighbourNode.cost + currentNode.fCost < neighbourNode.fCost)
            {
                neighbourNode.fCost = neighbourNode.cost + currentNode.fCost;
                queue.Enqueue(neighbourNode);
            }
        }
    }
}

搜索邻节点时有两种方式,四连通与八连通,前者是指向当前节点的上、下、左、右四个方向搜索邻节点,后者是指向当前节点的上、下、左、右、左上、右上、左下、右下八个方向搜索邻节点,多数情况下会使用八连通,本文也通过八连通的方式搜索邻节点。

/// <summary>
/// 获取指定节点的邻节点
/// </summary>
/// <param name="node">要获取邻节点的节点</param>
/// <returns>邻节点列表</returns>
public List<FlowFieldPathFinding_GridNode> GetNeighbouringNodes(FlowFieldPathFinding_GridNode node)
{
    List<FlowFieldPathFinding_GridNode> neighbours = new List<FlowFieldPathFinding_GridNode>();
    for (int i = -1; i <= 1; i++)
    {
        for (int j = -1; j <= 1; j++)
        {
            if (i == 0 && j == 0) continue;
            int x = node.x + i;
            int y = node.y + j;
            if (x >= 0 && x < this.x && y >= 0 && y < this.y)
                neighbours.Add(nodesDic[x * this.x + y]);
        }
    }
    return neighbours;
}

计价方式

每向正上、下、左右方向走一步代价为1,根据勾股定理,每向斜方向走一步代价为根号2,近似1.414,而为了便于计算、节省性能,将正方向移动一步的代价记为10,斜方向移动一步的代价记为14,都取int整数。

//计算两个节点之间的代价
private int CalculateCost(FlowFieldPathFinding_GridNode node1, FlowFieldPathFinding_GridNode node2)
{
    //取绝对值
    int deltaX = node1.x - node2.x;
    if (deltaX < 0) deltaX = -deltaX;
    int deltaY = node1.y - node2.y;
    if (deltaY < 0) deltaY = -deltaY;
    int delta = deltaX - deltaY;
    if (delta < 0) delta = -delta;
    //每向上、下、左、右方向移动一个节点代价增加10
    //每斜向移动一个节点代价增加14(勾股定理,精确来说是近似14.14~)
    return 14 * (deltaX > deltaY ? deltaY : deltaX) + 10 * delta;
}

流场生成

设置目标节点计算各节点的代价后生成流场,遍历节点的邻节点,如果邻节点的最终代价小于当前节点的最终代价,那么当前节点的方向便是当前节点指向邻节点的方向。若出现代价相同的情况,需要考虑哪一个邻节点更加接近目标节点。

/// <summary>
/// 生成流场
/// </summary>
public void GenerateFlowField(FlowFieldPathFinding_GridNode target)
{
    foreach (var node in nodesDic.Values)
    {
        List<FlowFieldPathFinding_GridNode> neighbourNodes = GetNeighbouringNodes(node);
        int fCost = node.fCost;
        FlowFieldPathFinding_GridNode temp = null;
        for (int i = 0; i < neighbourNodes.Count; i++)
        {
            FlowFieldPathFinding_GridNode neighbourNode = neighbourNodes[i];
            if (neighbourNode.fCost < fCost)
            {
                temp = neighbourNode;
                fCost = neighbourNode.fCost;
                node.direction = new Vector3(
                    neighbourNode.x - node.x, 0, neighbourNode.y - node.y);
            }
            else if (neighbourNode.fCost == fCost && temp != null)
            {
                if (CalculateCost(neighbourNode, target) < CalculateCost(temp, target))
                {
                    temp = neighbourNode;
                    fCost = neighbourNode.fCost;
                    node.direction = new Vector3(
                        neighbourNode.x - node.x, 0, neighbourNode.y - node.y);
                }
            }
        }
    }
}

根据流场方向移动

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/246364.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

人工智能在大型复杂机械产品装配状态检测自动化中的应用

尊敬的读者们&#xff0c;本文主要围绕“大型复杂机械产品装配状态检测自动化方案”开展讨论&#xff0c;从这个领域存在的问题和难度&#xff0c;以及基于人工智能、数字图像处理、机器人控制、装配机理等技术的自动化设计与实践方案。文章提出了数字化建模和智能识别模型构建…

[Springboot 源码系列] 浅析自动配置原理

文章目录 自动配置类原理AopAutoConfigurartion条件装配的底层原理ConditionalConditionalOnXxx 自动配置类原理 public class AutoConfApplication {public static void main(String[] args) {GenericApplicationContext context new GenericApplicationContext();context.re…

Git命令之本地分支与远程分支支关联/解除关联

目录 1.应用场景2.关联远程仓库中存在的分支3.解除本地分支与远程分支的关联 1.应用场景 在实际的工作生活中&#xff0c;往往需要将本地的分支和远程分支关联&#xff0c;这样我们就可以使用git pull命令来更新拉取最新的代码&#xff0c;并使用git push命令将自己本地的修改…

设备远程监控系统:实时对工厂设备状态进行监控

随着工业4.0时代的到来&#xff0c;工业物联网平台在工业领域的应用越来越广泛。设备远程监控系统作为工业物联网的重要组成部分&#xff0c;能够实时对工厂设备状态进行监控&#xff0c;提高生产效率和管理水平。本文将详细介绍工厂设备数据的采集方式以及设备远程监控系统的应…

企业寄件运费贵?企业如何将企业快递费用降至最低?

按照上述情况&#xff0c;如果使用闪侠惠递商家寄件服务&#xff0c;相较于原本的费用&#xff0c;会节省很大一部分的成本费用的&#xff0c;显而易见&#xff0c;这是非常便宜的。 闪侠惠递的寄件服务支持圆通、德邦、京东、顺丰等多家主流快递公司&#xff0c;并可以对比多…

Git-瑞吉外卖

什么是GIt? 分布式版本控制工具&#xff0c;用来管理源代码文件。分布式主要体现在两种仓库&#xff08;本地仓库、远程仓库&#xff09;。 git的作用&#xff1f; 代码回溯、版本切换&#xff08;切换不同框架&#xff09;、多人协作、远程备份 基本命令&…

虚拟化逻辑架构:KVM虚拟机通过OVS端口组实现网络连接

目录 一、实验 1.CentOS 7 安装 OpenVSwitch(构建RPM安装包&#xff09; 2.KVM虚拟机通过OVS端口组实现网络连接 二、问题 1.安装openvswitch-2.5.10报错 2.virt-install未找到命令 3.如何删除自定义网络 4.开机如何自动启动自定义网络 一、实验 1.CentOS 7 安装 Open…

双指针算法(一)

目录 移动零 复写零 快乐数 盛水最多的容器 双指针与单调性结合 有效三角形的个数 查找总价格为目标值的两个商品 两数之和 Ⅱ - 输入有序数组 双指针算法是通过定义两个指针不断单向移动来解决问题的一种算法。但双指针算法&#xff0c;是一个抽象的思想概念&#xf…

【C语言】超详解strncpystrncatstrncmpstrerrorperror的使⽤和模拟实现

&#x1f308;write in front :&#x1f50d;个人主页 &#xff1a; 啊森要自信的主页 ✏️真正相信奇迹的家伙&#xff0c;本身和奇迹一样了不起啊&#xff01; 欢迎大家关注&#x1f50d;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;>希望看完我的文章对你有小小的帮助&am…

智能优化算法应用:基于原子搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于原子搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于原子搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.原子搜索算法4.实验参数设定5.算法结果6.…

12.4~12.14概率论复习与相应理解(学习、复习、备考概率论,这一篇就够了)

未分配的题目 概率计算&#xff08;一些转换公式与全概率公式&#xff09;与实际概率 &#xff0c;贝叶斯 一些转换公式 相关性质计算 常规&#xff0c;公式的COV与P 复习相关公式 计算出新表达式的均值&#xff0c;方差&#xff0c;再套正态分布的公式 COV的运算性质 如…

FastAPI访问/docs接口文档显示空白、js/css无法加载

如图&#xff1a; 原因是FastAPI的接口文档默认使用https://cdn.jsdelivr.net/npm/swagger-ui-dist5.9.0/swagger-ui.css 和https://cdn.jsdelivr.net/npm/swagger-ui-dist5.9.0/swagger-ui-bundle.js 来渲染页面&#xff0c;而这两个URL是外网的CDN&#xff0c;在国内响应超…

五、Java核心数组篇

1.数组 概念&#xff1a; ​ 指的是一种容器&#xff0c;可以同来存储同种数据类型的多个值。 ​ 但是数组容器在存储数据的时候&#xff0c;需要结合隐式转换考虑。 比如&#xff1a; ​ 定义了一个int类型的数组。那么boolean。double类型的数据是不能存到这个数组中的&…

【RocketMQ】顺序消费消息实现原理分析

一、顺序消息概述 1.1、什么是顺序消息 顺序消息是指对于一个指定的 Topic &#xff0c;消息严格按照先进先出&#xff08;FIFO&#xff09;的原则进行消息发布和消费&#xff0c;即先发布的消息先消费&#xff0c;后发布的消息后消费。 1.2、顺序消息的类型 全局顺序消息 …

经典深度学习算法【1】:K-近邻算法(KNN)概述

最简单最初级的分类器是将全部的训练数据所对应的类别都记录下来&#xff0c;当测试对象的属性和某个训练对象的属性完全匹配时&#xff0c;便可以对其进行分类。但是怎么可能所有测试对象都会找到与之完全匹配的训练对象呢&#xff0c;其次就是存在一个测试对象同时与多个训练…

JVM虚拟机系统性学习-JVM调优之GC日志分析

JVM 调优 首先&#xff0c;为什么要 JVM 调优呢&#xff1f; JVM 调优的目的就是为了让应用程序使用最小的硬件消耗来承载更大的吞吐量 什么情况下需要 JVM 调优呢&#xff1f; 系统吞吐量下降&#xff0c;或系统延迟较高出现 OOMFull GC 频繁GC 停顿时间过长&#xff08;超…

UE虚幻引擎中程序无需运行也可调试

首先先新建一个蓝图类&#xff0c;在蓝图类中创建一个Custom event 事件&#xff0c;然后在右侧细节面板中搜索call in editor&#xff0c;编译保存之后&#xff0c;将该蓝图类拖拽到关卡场景中&#xff0c;在细节面板中即可看到该事件的按钮。

PE文件格式-PE文件头部

文章目录 PE文件头基本概念简介地址对齐 PE结构概述PE文件头部DOS MZ 头PE头&#xff08;NT头&#xff09;PE头标识 Signature标准PE头 IMAGE_FILE_HEADERMachineNumberOfSectionsTimeDateStampPointToSymbolTableNumberOfSymbolSizeOfOptionalHeaderCharacteristics 扩展PE头 …

亚信科技AntDB数据库——深入了解AntDB-M元数据锁的相关概念

AntDB-M在架构上分为两层&#xff0c;服务层和存储引擎层。元数据的并发管理集中在服务层&#xff0c;数据的存储访问在存储引擎层。为了保证DDL操作与DML操作之间的一致性&#xff0c;引入了元数据锁&#xff08;MDL&#xff09;。 AntDB-M提供了丰富的元数据锁功能&#xff…

【Hadoop】执行start-dfs.sh启动hadoop集群时,datenode没有启动怎么办

执行start-dfs.sh后&#xff0c;datenode没有启动&#xff0c;很大一部分原因是因为在第一次格式化dfs后又重新执行了格式化命令&#xff08;hdfs namenode -format)&#xff0c;这时主节点namenode的clusterID会重新生成&#xff0c;而从节点datanode的clusterID 保持不变。 在…