草庐IT

Unity实现A*寻路算法学习2.0

AlphaIcarus 2023-03-28 原文

二叉树存储路径节点

1.0中虽然实现了寻路的算法,但是使用List<>来保存节点性能并不够强

寻路算法学习1.0在这里:https://www.cnblogs.com/AlphaIcarus/p/16185843.html

更好的方法是使用堆(或者叫树)来代替列表存储节点
注意:这里使用数组来实现堆,而非使用链表实现堆
这里使用二叉树的方式来存储节点之间的关系

如果在树的末尾添加了一个较小的值,

那么需要和父节点比较大小,如果更小,则交换位置


然后再与父节点比较大小,如果小于父节点,则再次交换位置


如果大于父节点,则停止交换

那如果较小的元素被移除了又怎么排序呢?(之前说过因为Clsot值比较后有时需要重新设置父节点)
首先把树末尾的节点数据添加到树顶

然后与两个子节点比较大小,和更小的那一个交换位置

然后再与两个子节点比较大小,直到两个子节点都更大

那么如何获取相关的节点呢?这里的数字表示第几个节点而不是存储的具体数据

可以发现以下关系
如果获取某个节点 n (第 n 个节点)

那么该节点的父节点为 ( n - 1) / 2

左子节点为 2n + 1

右子节点为 2n + 2

在Unity中应用

新建脚本Heap,这是一个数据类型,用来代替List类型,由数组构成

public class Heap<T> where T : IHeapItem<T>	//继承了该接口之后,需要实现数据类型T比较大小的方法
{
    T[] items;  //泛型数组,可以为任何类型的数据 
    int currentItemCount;	//当前总共有多少元素
    public int Count		//属性,访问返回元素个数
    {
        get { return currentItemCount; }
    }
    public Heap(int maxHeapSize)    //构造器
    {
        items = new T[maxHeapSize];
    }
    public void Add(T item)     //添加新元素的方法
    {
        item.HeapIndex = currentItemCount;
        items[currentItemCount] = item;	//末尾添加新元素
        SortUp(item);				   //排序
        currentItemCount++;			    //元素总数+1
    }
    public T RemoveFirt()			//获取堆顶部元素并移除
    {
        T firstItem = items[0];		//取得顶部元素
        currentItemCount--;			//元素总数-1
        items[0] = items[currentItemCount];	//将最后一个元素移到顶部
        items[0].HeapIndex = 0;				//将该元素的索引设为0,即第一个元素
        SortDown(items[0]);					//下沉元素
        return firstItem;			//返回取得的顶部元素
    }
    public void UpdateItem(T item)
    {
        SortUp(item);
    }
    public bool Contains(T item)	//判断是否包含元素
    {
        return Equals(items[item.HeapIndex], item);
    }
    void SortDown(T item)     //下沉元素
    {
        while (true)	//一直循环,直到值小于左右子树或到最后位置
        {
            int childIndexLeft = item.HeapIndex * 2 + 1;	//左子树的索引
            int childIndexRight = item.HeapIndex * 2 + 2;	//右子树的索引
            int swapIndex = 0;
            if (childIndexLeft < currentItemCount)	//
            {
                swapIndex = childIndexLeft;
                if (childIndexRight < currentItemCount)
                {
                    if (items[childIndexLeft].CompareTo(items[childIndexRight]) < 0)
                    {
                        swapIndex = childIndexRight; 
                    }
                }
                if (item.CompareTo(items[swapIndex]) < 0)
                {
                    Swap(item, items[swapIndex]);
                }
                else
                {
                    return;
                }
            }
            else
            {
                return;
            }
        }
    }
    private void SortUp(T item) //上浮元素
    {
        int parentIndex = (item.HeapIndex - 1) / 2;     //父节点的位置
        while (true)	//循环直至数据比父节点大
        {
            T parentItem = items[parentIndex];
            if (item.CompareTo(parentItem) > 0) //如果新的数据比父节点更小(f值更小)
            {
                Swap(item, parentItem);		//交换位置
            }
            else
            {
                break;
            }
            parentIndex = (item.HeapIndex - 1) / 2;	//下次循环前再次验证父节点索引
        }
    }
    private void Swap(T itemA, T itemB)	//交换数组中两个元素的位置
    {
        items[itemA.HeapIndex] = itemB;
        items[itemB.HeapIndex] = itemA;
        int itemAIndex = itemA.HeapIndex;
        itemA.HeapIndex = itemB.HeapIndex;
        itemB.HeapIndex = itemAIndex;
    }
}
public interface IHeapItem<T> : IComparable<T> //接口,Heap必须实现该接口,IHeapItem继承可比较接口
{
    int HeapIndex { get; set; }
}

接下来是Node

public class Node : IHeapItem<Node>	//继承接口,需要实现能够比较Node大小的方法
{
    ......
    private int heapIndex;	//声明节点的索引
    ......
    public int HeapIndex	//属性
    {
        get { return heapIndex; }
        set { heapIndex = value; }
    }
    public int CompareTo(Node needToCompare)	//实现的比较大小的方法
    {
        //通过比较 F 值的大小,来判断节点的大小
        int compare = FCost.CompareTo(needToCompare.FCost);
        if (compare == 0)
        {
            compare = hCost.CompareTo(needToCompare.hCost);
        }
        return -compare;
    }
}

接下来是PathFinding

public class PathFinding : MonoBehaviour
{
    ......
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            //分别注释方法,运行后比较两个方法寻找相同路径所需的时间
            //FindPath(seeker.position, target.position);	//之前使用List的方法
            FindPathHeap(seeker.position, target.position);	//新的使用Heap的方法
        }
    }
    ......
    private void FindPathHeap(Vector3 startPos, Vector3 targetPos)
    {
        Stopwatch sw = new Stopwatch();	//计时器
        sw.Start();
        Node startNode = grid.NodeFromWorldPos(startPos);   //输入空间坐标,计算出处于哪个节点位置
        Node targwtNode = grid.NodeFromWorldPos(targetPos);
		//这里将List替换为了Heap,初始化Heap
        Heap<Node> openSet = new Heap<Node>(grid.MaxSize);          //用于存储需要评估的节点
        HashSet<Node> closedSet = new HashSet<Node>();  //用于存储已经评估的节点

        openSet.Add(startNode);

        while (openSet.Count > 0)   //如果还有待评估的节点
        {
            Node currentNode = openSet.RemoveFirt();  //获取其中一个待评估的节点
            closedSet.Add(currentNode);     //将该节点加入已评估的节点,之后不再参与评估
            if (currentNode == targwtNode)  //如果该节点为目标终点,就计算出实际路径并结束循环
            {
                sw.Stop();
                print("Path found: " + sw.ElapsedMilliseconds + "ms");
                RetracePath(startNode, targwtNode);
                return;
            }
            //如果该节点不是目标点,遍历该点周围的所有节点
            foreach (Node neighbor in grid.GetNeighbors(currentNode))
            {
                //如果周围某节点不能行走 或 周围某节点已经评估,为上一个节点,则跳过
                //                          说明某节点已经设置父节点
                if (!neighbor.walkable || closedSet.Contains(neighbor))
                {
                    continue;
                }
                //计算前起始点前往某节点的 gCost 值,起始点的 gCost 值就是0  
                //经过循环这里会计算周围所有节点的g值
                int newMovementCostToNeighbor = currentNode.gCost + GetDinstance(currentNode, neighbor);
                //如果新路线 gCost 值更小(更近), 或 某节点没有评估过(为全新的节点)
                if (newMovementCostToNeighbor < neighbor.gCost || !openSet.Contains(neighbor))
                {

                    neighbor.gCost = newMovementCostToNeighbor;             //计算某节点gCost
                    neighbor.hCost = GetDinstance(neighbor, targwtNode);    //计算某节点hCost
                    neighbor.parent = currentNode;                          //将中间节点设为某节点的父节点
                                                                            //如果存在某节点gCost更小的节点,会重新将中间节点设为某节点父节点
                    if (!openSet.Contains(neighbor))    //如果某节点没有评估过
                    {
                        openSet.Add(neighbor);          //将某节点加入待评估列表,在下一次循环进行评估,
                    }
                }
            }
        }
    }
    ......
}

接下来是MyGrid

public class MyGrid : MonoBehaviour
{
    public bool onlyPlayPathGizmos;	//是否只绘制路径,便于观察
    ......
    void Start()
    {
        nodeDiameter = nodeRadius * 2;
        gridSizeX = Mathf.RoundToInt(gridWorldSize.x / nodeDiameter); //计算出x轴方向有多少个节点
        gridSizeY = Mathf.RoundToInt(gridWorldSize.y / nodeDiameter); //计算出z轴方向有多少个节点
        CreateGrid();
    }
    public int MaxSize	//属性,返回地图路径点的数量
    {
        get { return gridSizeX * gridSizeY; }
    }
    ......
    private void OnDrawGizmos()
    {
        Gizmos.DrawWireCube(transform.position, new Vector3(gridWorldSize.x, 1, gridWorldSize.y));
        if (grid != null)
        {
            Node playerNode = NodeFromWorldPos(player.position);
            if (onlyPlayPathGizmos)
            {
                //只绘制路径
                if (path != null)
                {
                    foreach (Node node in path)
                    {
                        Gizmos.color = Color.yellow;
                        Gizmos.DrawCube(node.worldPos, Vector3.one * (nodeDiameter - 0.1f));
                    }
                }
            }
            else	//绘制地图所有点和路径
            {
                foreach (Node node in grid)
                {
                   ......
                }
            }
        }
    }
}

接下来为了体现两个方法的耗时差距

修改一下地图的大小和节点的大小

运行结果:


使用Heap的耗时

使用List的耗时

可以看见速度大概提高了三倍

有关Unity实现A*寻路算法学习2.0的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. ruby-on-rails - 如何在 Ruby on Rails 中实现由 JSF 2.0 (Primefaces) 驱动的 UI 魔法 - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。问题1)我想知道ruby​​onrails是否有功能类似于primefaces的gem。我问的原因是如果您使用primefaces(http://www.primefaces.org/showcase-labs/ui/home.jsf),开发人员无需担心javascript或jquery的东西。据我所知,JSF是一个规范,基于规范的各种可用实现,prim

  3. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  6. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  7. unity---接入Admob - 2

    目录1.AdmobSDK下载地址2.将下载好的unityPackagesdk导入到unity里​编辑 3.解析依赖到项目中

  8. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

随机推荐