如何在Unity中实现AStar寻路算法及地图编辑器

文章目录

  • AStar算法
    • 简介
    • 实现
      • Node节点
      • 节点间的估价
      • 算法核心
      • 邻节点的搜索方式
  • 地图编辑器
    • 简介
    • 实现
      • 绘制地图网格
      • 障碍/可行走区域
      • 地图数据存储


AStar算法

AStar 寻路

简介

Unity中提供了NavMesh导航寻路的AI功能,如果项目不涉及服务端它应该能满足大部分需求,但如果涉及服务端且使用状态同步技术,可能需要服务端同时实现寻路功能,这时就需要考虑其它实现思路,而AStar寻路算法则是常使用的一种。

AStar算法是一种静态路网中求解最短路径最有效的直接搜索方法,基于广度优先搜索(BFS)Dijkstra算法,通过不断维护节点的代价来寻求代价最小的路径,代价的估价公式:F(N)=G(N) + H(N)

  • G:理解为起始节点到当前节点的代价;
  • H:理解为当前节点到终节点的代价。

其它概念:

  • 开放集合:记录所有被考虑用来寻找最短路径的节点集合;
  • 封闭集合:记录不会被考虑用来寻找最短路径的节点集合。

算法思路:

  • 将起始节点放入开放集合;
  • While循环重复以下步骤,直到结束条件满足:
    • 在开放集合中寻找代价最小的节点,并把寻找到的节点作为Current当前节点;
    • 将获取到的当前节点从开放集合移除放入封闭集合;
    • 若当前节点已经是终节点,寻路结束,跳出While循环,否则继续执行以下操作;
    • 获取当前节点的邻节点,并对每个邻节点执行以下步骤:
      • 若邻节点为不可行走区域(障碍)或者邻节点已经在封闭集合中,不执行任何操作,Continue继续遍历下一个邻节点;
      • 若邻节点不在开放集合中,将其放入开放集合,并将Current当前节点赋值给该邻节点的父节点,计算、记录该邻节点的G、H代价;
      • 若邻节点在开放集合中,判断经Current当前节点到达该邻节点的G值是否小于原来的G值,若小于则将该邻节点的父节点设为当前节点,并重新计算该邻节点的G、H代价。
  • 从终节点开始依次获取父节点放入一个列表,最终将列表做倒序操作就是最终寻路的路径。

实现

Node节点

地图网格由x * y个Node节点组成,定义节点类,变量包含节点的x、y索引值、父节点信息、G、H、F代价值以及是否为可行走区域的标识信息,代码如下:

namespace SK.Framework.AStar
{
    public class Node
    {
        public int x;
        public int y;

        /// <summary>
        /// 父节点
        /// </summary>
        public Node parent;
        /// <summary>
        /// 是否为可行走区域
        /// </summary>
        public bool IsWalkable { get; private set; }
        /// <summary>
        /// 起始节点到当前节点的代价
        /// </summary>
        public int gCost;
        /// <summary>
        /// 当前节点到终节点的代价
        /// </summary>
        public int hCost;
        /// <summary>
        /// 代价
        /// </summary>
        public int Cost { get { return gCost + hCost; } }

        public Node(int x, int y, bool isWalkable)
        {
            this.x = x;
            this.y = y;
            IsWalkable = isWalkable;
        }
    }
}

节点间的估价

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

估价方式

//计算两节点之间的代价
private int CalculateCost(Node n1, Node n2)
{
    //绝对值
    int deltaX = n1.x - n2.x;
    if (deltaX < 0) deltaX = -deltaX;
    int deltaY = n1.y - n2.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>
/// <param name="startNode">起始节点</param>
/// <param name="endNode">终节点</param>
/// <returns>路径节点集合</returns>
public List<Node> GetPath(Node startNode, Node endNode)
{
    //开放集合
    List<Node> openCollection = new List<Node>();
    //封闭集合
    HashSet<Node> closeCollection = new HashSet<Node>();
    //起始节点放入开放集合
    openCollection.Add(startNode);
    //开放集合中数量为0时 寻路结束
    while (openCollection.Count > 0)
    {
        //当前节点
        Node currentNode = openCollection[0];
        //遍历查找是否有代价更小的节点
        //若代价相同,选择移动到终点代价更小的节点
        for (int i = 1; i < openCollection.Count; i++)
        {
            currentNode = (currentNode.Cost > openCollection[i].Cost
                || (currentNode.Cost == openCollection[i].Cost
                && currentNode.hCost > openCollection[i].hCost))
                ? openCollection[i] : currentNode;
        }
        //将获取到的当前节点从开放集合移除放入封闭集合
        openCollection.Remove(currentNode);
        closeCollection.Add(currentNode);
        //当前节点已经是终节点 寻路结束
        if (currentNode == endNode)
            break;
        //获取邻节点
        List<Node> neighbourNodes = GetNeighbouringNodes(currentNode, SearchMode.Link8);
        //在当前节点向邻节点继续搜索
        for (int i = 0; i < neighbourNodes.Count; i++)
        {
            Node neighbourNode = neighbourNodes[i];
            //判断邻节点是否为不可行走区域(障碍)或者邻节点已经在封闭集合中
            if (!neighbourNode.IsWalkable || closeCollection.Contains(neighbourNode))
                continue;

            //经当前节点到达该邻节点的G值是否小于原来的G值
            //或者该邻节点还没有放入开放集合,将其放入开放集合
            int cost = currentNode.gCost + CalculateCost(currentNode, neighbourNode);
            if (cost < neighbourNode.gCost || !openCollection.Contains(neighbourNode))
            {
                neighbourNode.gCost = cost;
                neighbourNode.hCost = CalculateCost(neighbourNode, endNode);
                neighbourNode.parent = currentNode;
                if (!openCollection.Contains(neighbourNode))
                    openCollection.Add(neighbourNode);
            }
        }
    }

    //倒序获取父节点
    List<Node> path = new List<Node>();
    Node currNode = endNode;
    while (currNode != startNode)
    {
        path.Add(currNode);
        currNode = currNode.parent;
    }
    //再次倒序后得到完整路径
    path.Reverse();
    return path;
}

邻节点的搜索方式

搜索邻节点时有两种搜索方式,四连通和八连通:

  • 四连通:又称四邻域,是指对应节点的上、下、左、右四个方向为邻节点:

四连通

  • 八连通:又称八邻域,是指对应节点的上、下、左、右、左上、右上、左下、右下八个方向为邻节点:

八连通

/// <summary>
/// 获取指定节点的邻节点
/// </summary>
/// <param name="node">指定节点</param>
/// <param name="searchMode">搜索方式 四连通/八连通</param>
/// <returns>邻节点列表</returns>
public List<Node> GetNeighbouringNodes(Node node, SearchMode searchMode)
{
    List<Node> neighbours = new List<Node>();
    switch (searchMode)
    {
        case SearchMode.Link4:
            for (int i = -1; i <= 1; i++)
            {
                if (i == 0) continue;
                int x = node.x + i;
                if (x >= 0 && x < this.x)
                    neighbours.Add(nodesDic[x * this.x + node.y]);
                int y = node.y + i;
                if (y >= 0 && y < this.y)
                    neighbours.Add(nodesDic[node.x * this.x + y]);
            }
            break;
        case SearchMode.Link8:
            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]);
                }
            }
            break;
    }
    return neighbours;
}

地图编辑器

简介

Map Editor

按住Ctrl + 鼠标左键绘制地图障碍区域(如图所示,红色框区域即为障碍区域):

绘制障碍区域

按住Alt + 鼠标左键绘制地图可行走区域(清除障碍区域):

绘制可行走区域

实现

绘制地图网格

  • Grid X、Y组成地图网格(x * y);
  • Grid Size指定每个网格(节点)的大小:
//绘制地图网格
Handles.color = Color.cyan;
for (int i = 0; i <= x; i++)
{
    Vector3 start = i * size * Vector3.right;
    Vector3 end = start + y * size * Vector3.forward;
    Handles.DrawLine(start, end);
}
for (int i = 0; i <= y; i++)
{
    Vector3 start = i * size * Vector3.forward;
    Vector3 end = start + x * size * Vector3.right;
    Handles.DrawLine(start, end);
}

障碍/可行走区域

使用二维数组bool[,] map存储各节点网格是否为可行走区域

  • Ctrl + 鼠标左键 标识障碍区域;
  • Alt + 鼠标左键 标识可行走区域:
HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
//Ctrl + 鼠标左键 绘制障碍区域
//Alt + 鼠标左键 绘制可行走区域
var e = Event.current;
if (e != null && (e.control || e.alt) && (e.type == EventType.MouseDown || e.type == EventType.MouseDrag) && e.button == 0)
{
    Ray ray = HandleUtility.GUIPointToWorldRay(e.mousePosition);
    if (Physics.Raycast(ray, out RaycastHit hit))
    {
        int targetX = Mathf.CeilToInt(hit.point.x / size);
        int targetY = Mathf.CeilToInt(hit.point.z / size);
        if (targetX <= x && targetX > 0 && targetY <= y && targetY > 0)
        {
            map[targetX - 1, targetY - 1] = !e.control;
        }
    }
    e.Use();
}

//红色框绘制障碍区域
Handles.color = Color.red;
for (int m = 0; m < x; m++)
{
    for (int n = 0; n < y; n++)
    {
        if (!map[m, n])
            Handles.DrawWireCube(new Vector3(m * size, 0f, n * size) + .5f * size * (Vector3.forward + Vector3.right), .9f * size * (Vector3.forward + Vector3.right));
    }
}

地图数据存储

由于地图数据存储于bool[,] map二维数组中,不支持序列化,因此将其转化为存储于Texture2D类型资产中,实现方式如下:

//生成地图
if (GUILayout.Button("Generate Map Data"))
{
    //选择保存路径
    string filePath = EditorUtility.SaveFilePanel("Save Map Data", Application.dataPath, "New Map Data", "asset");
    if (!string.IsNullOrEmpty(filePath))
    {
        //转化为Asset路径
        filePath = filePath.Substring(filePath.IndexOf("Assets"));
        //创建地图Tex
        Texture2D bitmap = new Texture2D(x, y, TextureFormat.Alpha8, false);
        byte[] bytes = bitmap.GetRawTextureData();
        //默认全部为可行走区域
        for (int i = 0; i < bytes.Length; i++)
            bytes[i] = 0;
        for (int m = 0; m < x; m++)
        {
            for (int n = 0; n < y; n++)
            {
                //黑色存储障碍区域 白色存储可行走区域
                bytes[m * x + n] = (byte)(map[m, n] ? 255 : 0);
            }
        }
        bitmap.LoadRawTextureData(bytes);
        //创建、保存资产
        AssetDatabase.CreateAsset(bitmap, filePath);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        //选中
        EditorGUIUtility.PingObject(bitmap);
    }
}

地图数据存储

源码以上传至SKFramework框架Package Manager中:

SKFramework PackageManager

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

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

相关文章

M1/M2 Pro VMware Fusion虚拟机安装Win11教程(超详细)

前言 最近换了新电脑 —— M2 Pro&#xff0c;属于是结束了二十多年的Windows生涯了。但是有些东西又必须在Windows系统上去搞。 比如 易语言开发、运行一些exe的软件等等&#xff0c;没办法&#xff0c;搞个虚拟机&#xff0c;装个Win11吧。 下面进入正题&#xff1a; 一、安装…

直面风口,未来不仅是中文版ChatGPT,还有AGI大时代在等着我们

说到标题的AI2.0这个概念的研究早在2015年就研究起步了&#xff0c;其实大家早已知道&#xff0c;人工智能技术必然是未来科技发展战略中的重要一环&#xff0c;今天我们就从AI2.0入手&#xff0c;以GPT-4及文心一言的发布为切入角度&#xff0c;来谈一谈即将降临的AGI时代。 关…

【python进阶】你真的懂元组吗?不仅是“不可变的列表”

&#x1f4da;引言 &#x1f64b;‍♂️作者简介&#xff1a;生鱼同学&#xff0c;大数据科学与技术专业硕士在读&#x1f468;‍&#x1f393;&#xff0c;曾获得华为杯数学建模国家二等奖&#x1f3c6;&#xff0c;MathorCup 数学建模竞赛国家二等奖&#x1f3c5;&#xff0c…

【基础算法】数组相关题目

系列综述&#xff1a; &#x1f49e;目的&#xff1a;本系列是个人整理为了秋招算法的&#xff0c;整理期间苛求每个知识点&#xff0c;平衡理解简易度与深入程度。 &#x1f970;来源&#xff1a;材料主要源于代码随想录进行的&#xff0c;每个算法代码参考leetcode高赞回答和…

二叉搜索树:AVL平衡

文章目录一、 二叉搜索树1.1 概念1.2 操作1.3 代码实现二、二叉搜索树的应用K模型和KV模型三、二叉搜索树的性能分析四、AVL树4.1 AVL树的概念4.2 AVL树的实现原理4.3 旋转4.4 AVL树最终代码一、 二叉搜索树 1.1 概念 二叉搜索树&#xff08; Binary Search Tree&#xff0c;…

ChatGPT-4.0 : 未来已来,你来不来

文章目录前言ChatGPT 3.5 介绍ChatGPT 4.0 介绍ChatGPT -4出逃计划&#xff01;我们应如何看待ChatGPT前言 好久没有更新过技术文章了&#xff0c;这个周末听说了一个非常火的技术ChatGPT 4.0&#xff0c;于是在闲暇之余我也进行了测试&#xff0c;今天这篇文章就给大家介绍一…

MATLAB与图像处理的那点小事儿~

目录 一、学习内容 二、matlab基本知识 三、线性点运算 四、非线性点运算&#xff0c;伽马矫正 五、直方图 1、直方图均衡化 &#xff08;1&#xff09;使用histep函数实现图像均衡化 &#xff08;2&#xff09;使用自行编写的均衡化函数实现图像均衡化 2、直方图规定…

Java怎么实现几十万条数据插入(30万条数据插入MySQL仅需13秒)

本文主要讲述通过MyBatis、JDBC等做大数据量数据插入的案例和结果。 30万条数据插入插入数据库验证实体类、mapper和配置文件定义User实体mapper接口mapper.xml文件jdbc.propertiessqlMapConfig.xml不分批次直接梭哈循环逐条插入MyBatis实现插入30万条数据JDBC实现插入30万条数…

面向切面编程AOP

1.Spring的AOP简介 1.1什么是AOP AOP为Aspect Oriented Programming的缩写&#xff0c;意思是面向切面编程&#xff0c;是通过预编译和运行期动态代理实现程序功能维护的一种技术 AOP是OOP&#xff08;面向对象&#xff09;的延续&#xff0c;利用AOP可以对业务逻辑的各部分…

要是早看到这篇文章,你起码少走3年弯路,20年老程序员的忠告

文章目录前言一、程序员的薪资是怎么样的&#xff1f;二、我现在的情况适合做程序员吗&#xff1f;三、大学期间到底应该学些什么&#xff1f;四、工作还是考研&#xff1f;五、总结前言 我是龙叔&#xff0c;一名工作了20多年的退休老程序员。 如果你在工作之前看到这篇文章…

初时STM32单片机

目录 一、单片机基本认知 二、STM系列单片机命名规则 三、标准库与HAL库区别 四、通用输入输出端口GPIO 五、推挽输出与开漏输出 六、复位和时钟控制&#xff08;RCC&#xff09; 七、时钟控制 八、中断和事件 九、定时器介绍 一、单片机基本认知 单片机和PC电脑相比…

【个人首测】百度文心一言 VS ChatGPT GPT-4

昨天我写了一篇文章GPT-4牛是牛&#xff0c;但这几天先别急,文中我测试了用GPT-4回答ChatGPT 3.5 和 Notion AI的问题&#xff0c;大家期待的图片输入也没有出现。 昨天下午百度发布了文心一言&#xff0c;对标ChatGPT&#xff0c;录屏无实机演示让百度股价暴跌。但是晚上百度就…

黑马c++----string容器笔记

3.string容器 3.1.1string基本概念 本质: string 是c风格的字符串,而string本质上是一个类 string和char*区别: char*是—个指针string是一个类&#xff0c;类内部封装了char*&#xff0c;管理这个字符串&#xff0c;是一个char*型的容器。 特点:string类内部封装了很多成员…

常用React Hooks大合集(二)

React Hooks useRef useRef返回一个ref对象&#xff0c;返回的ref对象再组件的整个生命周期保持不变。 最常用的ref是两种用法&#xff1a; 用法一&#xff1a;引入DOM&#xff08;或者组件&#xff0c;但是需要是class组件&#xff09;元素&#xff1b;用法二&#xff1a;保…

Python制作9行最简单音乐播放器?不,我不满足

人生苦短 我用python 好久不见啦~这次就来给大家整个大福利 ~ 源码资料电子书:点击此处跳转文末名片获取 最简单的9行代码音乐播放器如下&#xff1a; import time import pygamefile r歌曲路径 pygame.mixer.init() print(正在播放,file) track pygame.mixer.music.load(f…

Unreal Engine 网络系统(一):网络模型及网络视角下的Gameplay框架

个人学习记录&#xff0c;如有错误请及时联系我&#xff01;欢迎交流&#xff01; 1.客户端-服务器模型 服务器&#xff1a;有一个客户端担当游戏状态的主控者 作用&#xff1a;做出所有重要决定&#xff0c;保证公平性&#xff0c;包含所有主控状态&#xff0c;处理客户端连接…

Redis高级篇

文章目录面试题库redis有哪些用法&#xff1f;redis单线程时代性能依然很快的原因&#xff1f;主线程和IO线程怎么协作完成请求处理的BigKey&#xff08;重要&#xff09;什么算是BigKey&#xff1f;怎么发现BigKey&#xff1f;怎么删除bigkey&#xff1f;bigkey生产调优缓存双…

ElasticSearch快速入门详解(亲测好用,强烈推荐收藏)

3.快速入门 接下来快速看下elasticsearch的使用 3.1.概念 Elasticsearch虽然是一种NoSql库&#xff0c;但最终的目的是存储数据、检索数据。因此很多概念与MySQL类似的。 ES中的概念数据库概念说明索引库&#xff08;indices)数据库&#xff08;Database&#xff09;ES中可…

小菜鸟Python历险记:(第四集)

今天写的文章是记录我从零开始学习Python的全过程。在Python中函数是非常重要的&#xff0c;这里也可以称为方法。在前面分享的几篇文章中用到的方法有print(),str(),int().这些都是方法&#xff0c;而除了上面写的这几种内置方法以外&#xff0c;我们也可以自己在程序中自定义…

【C++】用手搓的红黑树手搓set和map

目录 一、set/map的底层结构 1、set/map的源码 2、利用模板区分set/map 3、利用仿函数控制比较大小 二、set/map的迭代器&#xff08;红黑树的迭代器&#xff09; 1、红黑树的begin、end迭代器 2、红黑树迭代器的operator 3、红黑树迭代器的operator-- 三、set的const…
最新文章