Unity 实现A* 寻路算法

news2025/2/24 10:11:03

前言

A* 寻路算法是什么

游戏开发中往往有这样的需求,让玩家控制的角色自动寻路到目标地点,或是让 AI 角色移动到目标位置,实际的情况可能很复杂,比如地图上有无法通过的障碍或者需要付出代价(时间或其他资源)才能通过的河流、沼泽等,想要让角色找到一条付出最小代价到达目标的路径,就需要使用一些特殊的算法,而 A寻路算法就是目前应用最广泛的寻路算法之一,unity asset store 上广受好评的 A* Pathfinding project 插件也是基于 A寻路算法实现的,简单来说:A* 算法是一种寻找最短路径并避开障碍物的算法


A* 算法的基本概念

至于为何A* 是寻路最受欢迎的选择,是因为它非常灵活,可以在各种环境中使用。

像 Dijkstra 算法一样,它可以用来找到最短的路径。

像贪心算法一样,它可以使用启发式来指导自己。在简单的情况下,它与贪心算法一样快:

在具有凹陷障碍物的示例中,A* 可以找到与Dijkstra算法找到的路径一样好的路径:

它成功的秘诀在于它结合了Dijkstra算法使用的信息(支持接近起点的顶点)和贪心算法使用的信息(支持接近目标的顶点)。

在谈论A *时使用的标准术语,g(n)表示从起点到任何顶点的路径的 确切成本 nh(n)表示从顶点到目标的启发式 估计成本  n

在上图中,黄色点h表示远离目标的顶点,蓝绿色点g表示远离起点的顶点。

A* 在从起点移动到目标时平衡两者。每次通过主循环,它检查 n具有最低的顶点 f(n) = g(n) + h(n)


A* 算法的实现原理

要实现 A算法,首先需要将纷繁复杂的游戏地图抽象成寻路网格(如上面的图),最简单的方式是将游戏地图划分为多个正方形单元或正多边形单元,也可以划分为非均匀的凸多边形,这些网格可以看做是一个个 “寻路点”,网格越精细,寻路的效果越好,但计算量也越大,所以针对实际的游戏环境,需要好好平衡一下性能和效果。


A算法的基本思想就是借助这些网格实现寻路,从起点开始遍历四周的点,寻找最有可能在最短路径上的点,并以这个点为基准继续向四周遍历,直至遍历到终点,路径也就找到了。

通过这个思想也可以看出,A算法其实只能得到一种近似最优解,实际上对于寻路问题,往往存在不止一个最优解,如果非要找出所有的解就只能遍历所有可能的路径一一比较,但这样效率太低,所以 A算法并不去遍历整个地图,而是只遍历了最短路径上的点和其周围的点,所以得到的是一种近似最优解。


那么遍历周围的点时怎样确定哪个点最有可能在最短路径上呢?这就是 A算法的核心:F=G+H

每个寻路点都有 F、G、H 这三个属性,F 可以理解为通过这个点的总代价,代价越低,这个点当然就更有可能在最短路径上。G 是从起点到这个点的代价,H 是从这个点到终点的代价,这两个代价加起来就是这个点的总代价,关于具体如何计算,下面给出示例。


我们还需要两个集合,一个是 open 集合,一个是 close 集合,open 集合里存放的是还未计算代价的点,close 集合里是已经计算过的点。开始时 open 集合里只有起点,close 集合没有元素,每次迭代将 open 集合里 F 最小的点作为基点,对于基点周围的相邻点做如下处理:
(1)如果这个点是障碍,直接无视。
(2)如果这个点不在 open 表和 close 表中,则加入 open 表
(3)如果这个点已经在 open 表中,并且当前基点所在路径代价更低,则更新它的 G 值和父亲
(4)如果这个点在 close 表中,忽略。
处理完之后将基点加入 close 集合。
当终点出现在 open 表中的时候,迭代结束。
如果到达终点前 open 表空了,说明没有路径可以到达终点。


正文

A算法实现

下面来动手实现最简单的 A* 算法,A* 算法针对实际开发有着相当多的变化,怎样设计跟游戏的需求有关,这里用 unity 来实现一个最基本的 2D 正方形网格寻路,实际开发中也可以直接使用 unity 的导航网格或者 A* Pathfinding Project 插件。

在这个实现中,我定义了一个 10x10 的网格,网格中有一些无法通过的障碍。

public class Point
{
    public int X;
    public int Y;
    public int F;
    public int G;
    public int H;
    public Point parent=null;

    public bool isObstacle = false;

    public Point(int x,int y)
    {
        X = x;
        Y = y;
    }

    public void SetParent(Point parent,int g)
    {
        this.parent = parent;
        G = g;
        F = G + H;
    }
}

这里定义了一个 Point 类代表每一个寻路点,X 和 Y 代表坐标,F、G、H 就是上面说的三个属性,isObstacle 代表这个点是否是障碍(无法通过),parent 则代表这个点的父亲结点,每当我们遍历到下一个可能在最短路径上的点时,就把它的父亲设为当前结点,这样寻路结束后我们可以从终点通过访问父亲结点一步步回溯到起点,将路径存储下来。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStar : MonoBehaviour
{
    public const int width = 10;
    public const int height = 10;

    public Point[,] map = new Point[height,width];
    public SpriteRenderer[,] sprites = new SpriteRenderer[height, width];//图片和结点一一对应

    public GameObject prefab;   //代表结点的图片
    public Point start;
    public Point end;

    void Start()
    {
        InitMap();
        //测试代码
        AddObstacle(2, 4);
        AddObstacle(2, 3);
        AddObstacle(2, 2);
        AddObstacle(2, 0);
        AddObstacle(6, 4);
        AddObstacle(8, 4);
        SetStartAndEnd(0, 0, 7, 7);
        FindPath();
        ShowPath();
    }

    public void InitMap()//初始化地图
    {
        for(int i=0;i<width;i++)
        {
            for (int j = 0; j < height; j++)
            {
                sprites[i, j] = Instantiate(prefab, new Vector3(i, j, 0),Quaternion.identity).GetComponent<SpriteRenderer>();
                map[i, j] = new Point(i, j);
            }
        }
    }

    public void AddObstacle(int x,int y)//添加障碍
    {
        map[x, y].isObstacle = true;
        sprites[x, y].color = Color.black;
    }

    public void SetStartAndEnd(int startX,int startY,int endX,int endY)//设置起点和终点
    {
        start = map[startX,startY];
        sprites[startX, startY].color = Color.green;
        end = map[endX, endY];
        sprites[endX, endY].color = Color.red;
    }

    public void ShowPath()//显示路径
    {
        Point temp = end.parent;
        while(temp!=start)
        {
            sprites[temp.X, temp.Y].color = Color.gray;
            temp = temp.parent;
        }
    }

    public void FindPath()
    {
        List<Point> openList = new List<Point>();
        List<Point> closeList = new List<Point>();
        openList.Add(start);
        while(openList.Count>0)//只要开放列表还存在元素就继续
        {
            Point point = GetMinFOfList(openList);//选出open集合中F值最小的点
            openList.Remove(point);
            closeList.Add(point);
            List<Point> SurroundPoints = GetSurroundPoint(point.X,point.Y);

            foreach(Point p in closeList)//在周围点中把已经在关闭列表的点删除
            {
                if(SurroundPoints.Contains(p))
                {
                    SurroundPoints.Remove(p);
                }
            }

            foreach (Point p in SurroundPoints)//遍历周围的点
            {
                if (openList.Contains(p))//周围点已经在开放列表中
                {
                    //重新计算G,如果比原来的G更小,就更改这个点的父亲
                    int newG = 1 + point.G;
                    if(newG<p.G)
                    {
                        p.SetParent(point, newG);
                    }
                }
                else
                {
                    //设置父亲和F并加入开放列表
                    p.parent = point;
                    GetF(p);
                    openList.Add(p);
                }
            }
            if (openList.Contains(end))//只要出现终点就结束
            {
                break;
            }
        }
    }


    public List<Point> GetSurroundPoint(int x,int y)//得到一个点周围的点
    {
        List<Point> PointList = new List<Point>();
        if(x>0&&!map[x-1,y].isObstacle)
        {
            PointList.Add(map[x - 1, y]);
        }
        if(y>0 && !map[x , y-1].isObstacle)
        {
            PointList.Add(map[x, y - 1]);
        }
        if(x<height-1 && !map[x + 1, y].isObstacle)
        {
            PointList.Add(map[x + 1, y]);
        }
        if(y<width-1 && !map[x , y+1].isObstacle)
        {
            PointList.Add(map[x, y + 1]);
        }
        return PointList;
    }


    public void GetF(Point point)//计算某个点的F值
    {
        int G = 0;
        int H = Mathf.Abs(end.X - point.X) + Mathf.Abs(end.Y - point.Y);
        if(point.parent!=null)
        {
            G = 1 + point.parent.G;
        }
        int F = H + G;
        point.H = H;
        point.G = G;
        point.F = F;
    }


    public Point GetMinFOfList(List<Point> list)//得到一个集合中F值最小的点
    {
        int min = int.MaxValue;
        Point point = null;
        foreach(Point p in list)
        {
            if(p.F<min)
            {
                min = p.F;
                point = p;
            }
        }
        return point;
    }
}

上面是 A* 算法的代码,这里使用了一张 100x100 像素的图片代表每一个结点,修改它们的颜色用来表示起点、终点、障碍和路径。在这里我计算的方式是每移动一个格子代价为 1,所以起点的 G 值为 0,每次遍历把 G+1,H 则是当前结点和终点在 x 轴和 y 轴上的差之和。

最终效果

寻路前

寻路结果


总结

A* 寻路有相当多可以扩展的地方,只要抓住核心,就是不断计算周围点的代价,找出花费最小代价到达终点的路径,这个代价可以针对各种复杂的情况采取不同的计算方法。

比如对于一个 FPS 游戏的 AI而言,游戏中玩家肯定会向火力范围内的敌人攻击,这时候如果为了走最短的路径而暴露在玩家的枪口下就得不偿失了,这时可以加大处在玩家攻击范围内的点的代价值,让 AI 在更短路径和受到攻击的风险之间做出权衡,或者某个地方有奖励道具,这时可以减少奖励道具附近的点的代价值,让 AI 更倾向于绕一些路去获取道具。

总之,理解了算法思想,才能为灵活运用于各种寻路情境打下基础。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

XShell免费版的安装配置教程以及使用教程(超级详细、保姆级)

目录 一、 XShell的作用 二、 下载免费版XShell 三、 安装XShell 四、使用XShell连接Linux服务器 一、 XShell的作用 XShell 是一种流行且简单的网络程序&#xff0c;旨在模拟虚拟终端。XShell可以在Windows界面下来访问远端不同系统下的服务器&#xff0c;从而比较好的达到…

11.落地:微服务架构灰度发布方案

前置知识 1.nacos 服务注册与发现 2.本地负载均衡器算法 3.gateway 网关 4.ThreadLocal 1.什么是灰度发布&#xff1f; 2.什么是灰度策略? 3.灰度发布落地方案有哪些 4.灰度发布架构设计原理 nginxlua&#xff1f; 5.如何基于GateWayNacos构建灰度环境 6.GateWay负载均衡…

【云原生 • Kubernetes】认识 k8s、k8s 架构、核心概念点介绍

目录 一、Kubernetes 简介 二、Kubernetes 架构 三、Kunbernetes 有哪些核心概念&#xff1f; 1. 集群 Cluster 2. 容器 Container 3. POD 4. 副本集 ReplicaSet 5. 服务 service 6. 发布 Deployment 7. ConfigMap/Secret 8. DaemonSet 9. 核心概念总结 一、Kubern…

java程序员转正述职报告PPT

新公司转正述职报告&#xff0c;花了些时间准备了ppt和讲稿&#xff0c;这里分享一下 述职报告 时间过得很快&#xff0c;转眼就已经三个月了&#xff0c;三个月时间不长&#xff0c;完成的工作也有限&#xff0c;但是在这些工作中&#xff0c;我也学到了很多&#xff0c;现在…

Linux命令大全:2W多字,一次实现Linux自由

前言 大家好&#xff0c;我是40岁老架构师尼恩&#xff0c;Linux 的学习对于一个程序员的重要性是不言而喻的。 学好它却是程序员必备修养之一。 同时&#xff0c;也是很多公司的面试题。 比如说&#xff0c;曾有一个网易的面试题是&#xff1a; 聊聊&#xff1a;你常用的几…

docker入门,这一篇就够了。

Docker入门&#xff0c;这一篇就够了。 Docker容器虚拟化平台。 前言 接触docker很长时间了&#xff0c;但是工作中也没有用到&#xff0c;所以总是学了忘&#xff0c;忘了学。不过这次&#xff0c;我打算跟大家分享一下我的学习历程&#xff0c;也算是我的独特的复习笔记&…

双目三维重建系统(双目标定+立体校正+双目测距+点云显示)Python

双目三维重建系统(双目标定立体校正双目测距点云显示)Python 目录 双目三维重建系统(双目标定立体校正双目测距点云显示)Python 1.项目结构 2. Environment 3.双目相机标定和校准 (0) 双目摄像头 (1) 采集标定板的左右视图 (2) 单目相机标定和校准 (3) 双目相机标定和…

毕业论文案例-LDA主题模型实现文本聚类

本文结构框架引言LDA主题模型的预备知识&#xff08;1&#xff09;多项式分布 Multinomial Distribution&#xff08;2&#xff09;狄利克雷分布 Dirichlet Distribution&#xff08;3&#xff09;共轭分布 Conjugate Distribution&#xff08;4&#xff09;吉普斯采样 Gibbs S…

springboot整合webSocket(看完即入门)

webSocket1、什么是webSocket&#xff1f;2、webSocket可以用来做什么?3、webSocket协议4、服务端WebSocket操作类5、客户端1、什么是webSocket&#xff1f; WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单&am…

100天精通Python(可视化篇)——第77天:数据可视化入门基础大全(万字总结+含常用图表动图展示)

文章目录1. 什么是数据可视化&#xff1f;2. 为什么会用数据可视化&#xff1f;3. 数据可视化的好处&#xff1f;4. 如何使用数据可视化&#xff1f;5. Python数据可视化常用工具1&#xff09;Matplotlib绘图2&#xff09;Seaborn绘图3&#xff09;Bokeh绘图6. 常用图表介绍及其…

【Windows】六种正确清理C盘的方法,解决你的红色烦恼

如何正确的清理C盘前言清理方法1. 利用Windows自己附带的磁盘清理工具2. 开启自动清理3. 通过“配置存储感知或立即运行”来清理4. 管理C盘中的程序5. 系统文件夹转移6. 将C盘现有内容转移到别的盘参考链接前言 Windows操作系统一般是安装在磁盘驱动器的C盘中&#xff0c;运行…

D435i相机的标定及VINS-Fusion config文件修改

引言 当我们想使用D435i相机去跑VINS-Fusion时&#xff0c;如果不把标定过的相机信息写入config文件中就运行&#xff0c;这样运动轨迹会抖动十分严重&#xff0c;里程计很容易漂。接下来将介绍如何标定D435i相机&#xff0c;并设置VINS-Fusion的config文件。 一 标定前的准备…

k8s中job与cronjob使用详解

一、前言 job,顾名思义就是任务,job的概念在很多框架中都有,而且实际业务场景中也使用非常广泛,比如大家熟悉的hadoop,客户端可以向集群提交一个job,然后集群根据一定的调度策略来处理这个job; k8s中的job,主要用于批量处理的业务场景,比如像那种短暂的一次性任务(每个…

java 代码样式为什么需要事务,讲述Spring5事务几种方式 认识API

首先 在上一文java Spring5 搭建操作数据库事务环境中 我们搭建了一个事务的业务场景 然后 打开项目 我们继续 先看到数据库表 看好两个人的余额 然后 来到senvice层下的transfAccoSenvice 将里面的 transferAccounts方法 更改如下 //转账方法 public void transferAccounts…

Vue的路由配置(Vue2和Vue3的路由配置)

系列文章目录 Tips&#xff1a;使用Vue3开发项目已经有一段时间了&#xff0c;关于Vue2的路由是如何一步一步搭建的都快要忘记了&#xff0c;今天写着篇文章主要就是回顾一下&#xff0c;在Vue2和Vue3中我们是如何一步一步的配置路由的。 提示&#xff1a;最好的进步就是有闲暇…

如何知道你的推荐流每条数据是通过哪种策略召回?

大家好&#xff0c;我是空空star&#xff0c;本篇带你了解下C站PC首页推荐流召回策略。 文章目录前言一、utm_medium二、召回策略1.user_follow_bbs&#xff1a;用户关注社区的红包帖子召回2.user_follow&#xff1a;用户关注召回3.top_blink&#xff1a;热门blink召回4.hot&am…

滚动条样式修改

前言 浏览器中的滚动条样式大家一定都不陌生&#xff0c;其样式并不好康。可能很多小伙伴还不知道&#xff0c;这个东东的样式也可以修改&#xff08;仅支持部分现代浏览器&#xff09;&#xff0c;本次就来带大家用 CSS 修改一下它的样式。 一、认识滚动条 首先我们先来简单…

(一)卷积神经网络模型之——LeNet

目录LeNet模型参数介绍该网络特点关于C3与S2之间的连接关于最后的输出层子采样参考LeNet LeNet是一个用来识别手写数字的最经典的卷积神经网络&#xff0c;是Yann LeCun在1998年设计并提出的。Lenet的网络结构规模较小&#xff0c;但包含了卷积层、池化层、全连接层&#xff0…

【前端进阶】-TypeScript高级类型 | 泛型约束、泛型接口、泛型工具类型

前言 博主主页&#x1f449;&#x1f3fb;蜡笔雏田学代码 专栏链接&#x1f449;&#x1f3fb;【TypeScript专栏】 前两篇文章讲解了TypeScript的一些高级类型 详细内容请阅读如下&#xff1a;&#x1f53d; 【前端进阶】-TypeScript高级类型 | 交叉类型、索引签名类型、映射类…

直接在前端调用 GPT-3 API

〇、效果展示 一、代码&#xff1a;ask.html app.js ask.html&#xff08;内嵌css&#xff09; <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>ChatGPT Web Example</title><style>/* 你的 CSS 代码 */bod…