游戏知识学习——【A*算法】


前言

A*算法?具体实现?主要步骤?特此学习并记录一下自己的理解


A*算法
B站:A*算法——讲解细致,推荐观看和理解源码


一、寻路算法:

1)广度优先遍历:

从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点

2)Dijkstra算法

计算每一个节点距离起点的移动代价
  f ( x ) = g ( x )   \ f(x)=g(x) \,  f(x)=g(x)

3)最佳优先搜索

预先计算出每个节点到终点的预估代价
  f ( x ) = h ( x )   \ f(x)=h(x) \,  f(x)=h(x)


二、A*算法

1)概念

A*算法是寻路算法的一种,是基于启发式函数改变节点选择规则的搜索。
  f ( x ) = g ( x ) + h ( x )   \ f(x)=g(x)+h(x) \,  f(x)=g(x)+h(x)

  • f(x)是当前节点的总代价,每次遍历总代价最小或者总代价相同但h(x)最小的为新的当前节点
  • g(x) 是当前节点距离起点的移动代价。
  • h(x)是当前节点距离终点的预估代价,也叫乐观代价。它总是小于等于实际到终点的代价,是A*算法的启发函数

注意:不考虑某些区域权值过大等问题,只考虑路径最短的情况,状态就是节点,代价就是距离

2)距离

代价是根据距离来定的,最常见的距离定义有:

  1. 曼哈顿距离:不能走斜线,只能按照固定的轴
      h ( x ) = ∣ s t a r t . x − e n d . x ∣ + ∣ s t a r t . y − e n d . y ∣   \ h(x)=|start.x-end.x|+|start.y-end.y| \,  h(x)=start.xend.x+start.yend.y

  2. 欧几里得距离:能走斜线,但计算要复杂一些,造成更大的性能开销
      h ( x ) = ( s t a r t . x − e n d . x ) 2 + ( s t a r t . y − e n d . y ) 2   \ h(x)=\sqrt{(start.x-end.x)^{2}+(start.y-end.y)^{2}}\,  h(x)=(start.xend.x)2+(start.yend.y)2

3)实现

主要步骤

  • 制作地图:随机生成或者自定义生成(数组即可)
  • 节点定义:定义一个NodeBase class类,为了翻转链表(从终点又找到起点)要设置节点的父节点,为了计算代价要定义G,H,F,为了遍历周围节点要获取距离并找到临近节点。
public class NodeBase{
	//每个节点内部保存临近节点,有一定开销
	public List<NodeBase> Neighbors { get; protected set; }
	//获取距离的方法以及坐标属性
	public interface ICoords {
    	public float GetDistance(ICoords other);
    	public Vector2 Pos { get; set; }
	}
	//父节点关联
	public NodeBase Connection { get; private set; }
	public void SetConnection(NodeBase nodeBase) {
		Connection = nodeBase;
	}
	//定义G代价
	public float G { get; private set; }
	public void SetG(float g) {
		G = g;
	}
	//定义H代价
	public float H { get; private set; }
	public void SetH(float h) {
		H = h;
	}
	//定义F代价
	public float F => G + H;
}
  • 计算代价:
    GetDistance() 获取距离的方法可以直接利用坐标进行距离计算,不同的距离公式计算的方式不同
    G(x) 直接由起点开始进行+1操作的计算,根据路径进行更新
    H(x) 直接利用坐标进行计算

  • 搜寻路径(包括找到最佳路径后反转链表):

//具体翻看视频中的源码
public static class Pathfinding {
	public static List<NodeBase> FindPath(NodeBase startNode, NodeBase targetNode) {
		//开集存储未探索周围节点,遍历前将起点放入开集
    	var toSearch = new List<NodeBase>() { startNode };
        //闭集存储已遍历节点
        var processed = new List<NodeBase>();
		
		//Any判断List是否为空,为空或者找到终点会跳出循环
        while (toSearch.Any()) {
        	//查找开集中的F(x)最小或者F(x)相同但H(x)最小
			var current = toSearch[0];
			foreach (var t in toSearch) 
				if (t.F < current.F || t.F == current.F && t.H < current.H) current = t;
			
			//遍历后找到开集中F(x)最小的节点加入闭集	
            processed.Add(current);
            toSearch.Remove(current);
            
			//如果当前节点等于终点节点(找到终点)
            if (current == targetNode) {
            	//这里只是用list简单存储路径,并未进行翻转
            	//提供两种思路,栈存储再弹出或者直接翻转list数组(或者进行索引直接输出)
				var currentPathTile = targetNode;
				var path = new List<NodeBase>();
				//一直查找到起点位置
				while (currentPathTile != startNode) {
					path.Add(currentPathTile);
					currentPathTile = currentPathTile.Connection;
				}
                return path;
			}    
			
			//由于节点内部保存了临近节点,所以直接限制这些节点不是障碍并且不在闭集中(闭集默认不会修改,路径)
			//两种情况:第一种在开集中,第二种未遍历,不在开集和闭集中
			//1,开集中的临近节点要判断是否进行更新
			//2,未遍历的节点要添加进开集
			foreach (var neighbor in current.Neighbors.Where(t => t.Walkable && !processed.Contains(t))) {
				var inSearch = toSearch.Contains(neighbor);
				//计算current周围neighbor的距离G(x)
				var costToNeighbor = current.G + current.GetDistance(neighbor);
				//不管是否在开集中都要更新临近节点的G(x)和父节点
				if (!inSearch || costToNeighbor < neighbor.G) {
				
					neighbor.SetG(costToNeighbor);
					neighbor.SetConnection(current);
					if (!inSearch) {
					//如果只是未遍历节点,还要设置H,并把此节点加入开集
					neighbor.SetH(neighbor.GetDistance(targetNode));
                    toSearch.Add(neighbor);
                    }
                }
            }
        }
        return null;
    }

核心伪代码

创建开集和闭集,并将起点加入闭集
while循环,只要开集不为空或者当前节点不是终点
	获取当前节点为开集中F(x)最小||F(x)相同但H(x)最小,移动到闭集中
	如果当前节点为终点,找到最佳路径,回溯并跳出循环
	如果不是终点,就遍历周围不在闭集且不是障碍的节点
		如果这个节点在开集中,计算G(x)比原来更优,更新父节点和G(x)
		否则如果这个节点未加入两个集合中,设置节点状态,将节点加入开集
如果开集为空,说明不存在路径,返回null	

三、简单实现A*算法

在unity中创建一个空物体挂载即可运行,手动修改地图

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

public class easy : MonoBehaviour
{
    //1,地图定义,直接二维数组
    public static int wide = 5;
    public static int height = 5;
    //有路的情况
    //public static int[,] tail = new int[,]
    //{
    //    {0,0,-1, 0, 0 },
    //    {0,0, 0, 0, 0 },
    //    {0,0,-1,-1, 0 },
    //    {0,0,-1, 0, 0 },
    //    {0,0,-1, 0, 0 },

    //};
    //没路的情况
    public static int[,] tail = new int[,]
    {
        {0,0,-1, 0, 0 },
        {0,0, -1, 0, 0 },
        {0,0,-1,-1, 0 },
        {0,0,-1, 0, 0 },
        {0,0,-1, 0, 0 },

    };
    //2,定义节点
    public class NodeBase
    {
        //构造函数
        public NodeBase(Vector2 _pos)
        {
            this.pos = _pos;
            Parent = null;
        }
        //父节点
        public NodeBase Parent;
        //坐标
        public Vector2 pos;
        //距离,未定义属性去增加代码复杂度
        public float G { get; set; }
        public float H { get; set; }
        public float F { get; set; }
    }

    //3,计算代价,曼哈顿距离,坐标格子之间距离为一,没有必要写新方法去获取距离

    List<NodeBase> AstarFindWay(NodeBase startNode, NodeBase endNode)
    {
        //开集存储未探索周围节点,遍历前将起点放入开集
        var toSearch = new List<NodeBase>() { startNode };
        //闭集存储已遍历节点
        var processed = new List<NodeBase>();

        //Any判断List是否为空,为空或者找到终点会跳出循环
        while (toSearch.Count > 0)
        {
            //查找开集中的F(x)最小或者F(x)相同但H(x)最小
            var current = toSearch[0];
            foreach (var t in toSearch)
                if (t.F < current.F || t.F == current.F && t.H < current.H) current = t;

            //遍历后找到开集中F(x)最小的节点加入闭集	
            processed.Add(current);
            toSearch.Remove(current);

            //如果当前节点等于终点节点(找到终点)
            if (isEqualNode(current,endNode))
            {
                //这里只是用list简单存储路径,并未进行翻转
                //提供两种思路,栈存储再弹出或者直接翻转list数组(或者进行索引直接输出)
                var currentPathTile = current;
                var path = new List<NodeBase>();
                //一直查找到起点位置
                while (!isEqualNode(currentPathTile, startNode))
                {
                    path.Add(currentPathTile);
                    currentPathTile = currentPathTile.Parent;
                }
                return path;
            }
            //拿到节点的临近节点的集合
            List<NodeBase> Neighbors = GetNeighbors(current);
           
            //两种情况:第一种在开集中,第二种未遍历,不在开集和闭集中
            //1,开集中的临近节点要判断是否进行更新
            //2,未遍历的节点要添加进开集
            foreach (var neighbor in Neighbors.Where(t => !Contains(processed,t)))
            {
                var inSearch = Contains(toSearch,neighbor);
                //计算current周围neighbor的距离G(x)
                var costToNeighbor = current.G + 1;

                if (inSearch)
                {
                    if (costToNeighbor < neighbor.G)
                    {
                        neighbor.G = costToNeighbor;
                        neighbor.Parent = current;
                        neighbor.F = neighbor.G + neighbor.H;
                    }
                }
                else
                {
                    neighbor.Parent = current;
                    neighbor.G = costToNeighbor;
                    //如果只是未遍历节点,还要设置H,并把此节点加入开集
                    neighbor.H = Mathf.Abs(neighbor.pos.x - endNode.pos.x) + Mathf.Abs(neighbor.pos.y - endNode.pos.y);

                    neighbor.F = neighbor.G + neighbor.H;
                    toSearch.Add(neighbor);
                }
            }
        }
        return null;
    }
    //临近节点只考虑前后左右
    List<NodeBase> GetNeighbors(NodeBase currentNode)
    {
        Vector2 up = new Vector2(currentNode.pos.x, currentNode.pos.y + 1);
        Vector2 down = new Vector2(currentNode.pos.x, currentNode.pos.y - 1);
        Vector2 left = new Vector2(currentNode.pos.x - 1, currentNode.pos.y);
        Vector2 right = new Vector2(currentNode.pos.x + 1, currentNode.pos.y);

        List<NodeBase> nodeList = new List<NodeBase>();
        if (isValidPos(up))
        {
            nodeList.Add(new NodeBase(up));
        }
        if (isValidPos(down))
        {
            nodeList.Add(new NodeBase(down));
        }
        if (isValidPos(left))
        {
            nodeList.Add(new NodeBase(left));
        }
        if (isValidPos(right))
        {
            nodeList.Add(new NodeBase(right));
        }
        return nodeList;
    }
    //判断这个坐标是否有节点并且可移动
    bool isValidPos(Vector2 pos)
    {
        if (pos.x < 0 || pos.x > wide - 1 || pos.y < 0 || pos.y > height - 1)
        {
            return false;
        }
        return tail[(int)pos.x, (int)pos.y] == 0 ? true : false;
    }

    //判断相等
    bool isEqualNode(NodeBase a,NodeBase b)
    {
        return (int)a.pos.x == (int)b.pos.x && (int)a.pos.y == (int)b.pos.y;
    }
    //坐标相同说明一样
    bool Contains(List<NodeBase> nodeList,NodeBase nodeBase)
    {
        for(int i = 0; i < nodeList.Count; ++i)
        {
            if (isEqualNode(nodeBase, nodeList[i]))
            {
                return true;
            }
        }
        return false;
    }


    private void Start()
    {
        //创建起点和终点,调用搜寻路径方法
        NodeBase startNode = new NodeBase(new Vector2(0, 0));
        NodeBase endNode = new NodeBase(new Vector2(4, 4));

        List<NodeBase> AstarNode = AstarFindWay(startNode, endNode);
        //输出路径
        if (AstarNode != null)
        {
            string str = "最佳路径:";
            for (int i = AstarNode.Count - 1; i >= 0; --i)
            {
                str += " [" + (int)AstarNode[i].pos.x + "," + (int)AstarNode[i].pos.y + "] ";
            }
            Debug.Log(str);
        }
        else
        {
            Debug.Log("八嘎呀路,死路一条");
        }
        
    }
}

有路的情况:
有路
无路的情况:
无路


总结

实现简单A*算法用到的很多方法需要重写,==,List的Contains(),临近节点等都需要判断坐标去实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
基于A*算法的迷宫小游戏开发,可以让玩家面对迷宫的挑战,通过智慧和策略找到迷宫的出口。 首先,我们需要设计一个迷宫地图。可以采用多种方式生成迷宫地图,如随机生成、手动设计或者使用迷宫生成算法。迷宫地图由起点、终点以及迷宫墙壁组成。 接下来,我们使用A*算法来寻找最佳路径。A*算法是一种启发式搜索算法,通过估计每个节点到目标点的距离来决定搜索方向。在实现A*算法时,需要定义一个启发函数来评估节点的价值,以便选择最优的路径。在该游戏中,可以使用曼哈顿距离或欧几里得距离作为启发函数。 当玩家开始游戏后,可以使用方向键或鼠标来控制角色移动。同时,在游戏界面上显示迷宫地图和玩家的当前位置。 在实现A*算法时,需要考虑一些特殊情况。比如,如何处理墙壁、如何处理无法到达的位置等。可以采用合适的数据结构,如优先队列或堆栈,来实现算法的搜索和路径的存储。 最后,为了增加游戏的趣味性和挑战性,可以在迷宫中添加一些道具或陷阱,用来干扰玩家的寻路过程。比如,道具可以提供额外的移动能力,而陷阱则会减慢玩家的速度。 通过以上方法,基于A*算法的迷宫小游戏可以提供给玩家一个有趣而挑战的寻路体验。同时,这个游戏也可以帮助玩家锻炼逻辑思维和空间认知能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

豪华落尽见真(ಡωಡ)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值