unity使用Dijkstra算法实现自动寻路

Posted 爱上游戏开发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity使用Dijkstra算法实现自动寻路相关的知识,希望对你有一定的参考价值。

前言

最近开始做新的功能模块——章节关卡。在做的过程中看到一个要求:根据当前关卡位置,点击任意已开放的关卡,主角自动寻路至该处。

数据准备

关卡配置表如下:

map = 
	[1] =  coordinate =  1, 1, , link =  2, 3, , ,
	[2] =  coordinate =  2, 1, , link =  1, 3, , ,
	[3] =  coordinate =  3, 2, , link =  4, 3, , ,
	[4] =  coordinate =  8, 2, , link =  2, 3, , ,
	[5] =  coordinate =  3, 3, , link =  2, 3, , ,
	[6] =  coordinate =  5, 3, , link =  2, 3, , ,
	[7] =  coordinate =  9, 3, , link =  2, 3, , ,
	[8] =  coordinate =  2, 4, , link =  2, 3, , ,

map的key值代表第X关。
coordinate代表对应关卡的坐标位置,例如:2,1代表坐标位置为x=2,y=1。
link代表与之相连的关卡,2,3代表与关卡2和关卡3相连。(即表示有通路)

尝试1

看到自动寻路,第一反应就是AStar算法。毕竟自动寻路嘛,那肯定得是A了。上次使用A还是在上次…enen,还是几年前上学时候,当时也只是学习了该算法的思路伪代码。要说用到实践里,那就是一个鸡蛋了。加上这么长时间过去了,只问其名,却已忘记其身,不得不借助www:度娘,我来了~。
首先找一篇通俗易懂的文章,看看算法的思路吧,代码的事后面再说~
先说下这个算法:A*算法主要用于求最短路径。算法的主要思想是:
参考自:https://blog.csdn.net/Zhouzi_heng/article/details/115035298
(1) 把起点加入 open list 。

(2) 重复如下过程:

      a.  遍历 open list ,查找 F 值最小的节点,把它作为当前要处理的节点。

      b.  把这个节点移到 close list 。

      c.  对当前方格的 8 个相邻方格的每一个方格?            

       ◆ 如果它是不可抵达的或者它在 close list 中,忽略它。否则,做如下操作。

       ◆ 如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。

       ◆ 如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。如果是这样,把它的父亲设置为当前方格,并重新计算它                                                                的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。

     d.停止,当你

       ◆把终点加入到了 open list 中,此时路径已经找到了,或者

       ◆查找终点失败,并且 open list 是空的,此时没有路径。

(3)保存路径。从终点开始,每个方格沿着父节点移动直至起点,这就是你的路径。

详情可见:推荐一篇大佬的A*算法文章:https://blog.csdn.net/xinzhilinger/article/details/119643810

搞懂了算法的思想后,我们其实是不需要自己写代码的,因为网上已经有很多成功的案例了,我们只需要站在巨人的肩膀上操作,去使用它。找到一个A算法的脚本如下:
https://mp.csdn.net/mp_download/manage/download/UpDetailed
那么怎么去使用它呢?
由于A
算法是在网格上寻路的,因此我们需要先创建地图网格,使用Star.New(),观察发现Astar.cs脚本发现,需要的参数是String该网格是由网格的长宽,以及每个位置的信息(是否能通过,1代表可通过0代表不可通过)构成的,类似于这样:

第一行的两个值分表达标地图网格的长宽,除去第一行后可以发现,剩下的是一个10*10的网格,每个格子上有一个数字(值为0或者1),代表是否可以通过。
那么,第一步,我们需要先将上面给我们的配置信息转换为对应的网格字段,方法如下:


--判断某个位置是路还是障碍物
local function CheckRoadOrBarrier(x,y)
	for k,v in pairs(map) do
		local coordinate = v.coordinate
		if coordinate[1] == x and coordinate[2] == y then
			return 1--是路
		end
	end
	return 0--障碍物
end
local fu
nction CreateMapData()
    --长宽
    local width = 10
    local heigth = 10
    local map = string.format("%s,%s",width,heigth)
    for i=1,heigth do
        map = map.."\\n"
        for j=1,width do
            if j == 1 then
                map = string.format("%s%s",map,CheckRoadOrBarrier(i,j))
            else
                map = string.format("%s,%s",map,CheckRoadOrBarrier(i,j))
            end
        end
    end
    printlog(map,"生成的地图信息????")
    return map
end

网格数据转换好后,我们需要开始初始化Astar算法:

local function InitAStar()
    current = AStarPoint.New(0,0)--默认初始位置在(0,0)坐标点
    local map = CreateMapData()
    astar = AStar.New(map)
    astar:SetMapWidthAndHeigth(10, 10)
end

接下来,是时候展现我们真正的技术了:我们开始真正使用Astar算法了。

local function GetShortPath(_x,_y,_finishfunc)
    local _next = AStarPoint.New(_x,_y)
    local path = astar:FindPath(current, _next)
    if path == nil or path.Count == 0 then
        _finishfunc(false)
        return
    else
        _finishfunc(true)
    end
end

local targetPos = Vector2.New(4,6)--目标位置,假设是(4,6)点,实际应用中以鼠标点击位置为准
GetShortPath(targetPos.x,targetPos.y,function (_bool)
    if _bool then
        --找到最短路径,可以前往目的地
    else
        --没有通路,不能前往目的地
    end
end)

运行程序后我发现,额,貌似有点尴尬,这个算法和我的需求不符合,我的需求只知道哪些点之间有路,
用Astar来寻路的话,反而将问题复杂化了,最主要是不可行,典型的没读懂题意。

网格可视化实现

一再陷入沉思中,既然算法还没想好,那就先按照配置,把关卡生成出来再试着找解决办法吧:

首先需要按照制定坐标生成关卡图标:

local function ShowLevelIcon()
    --关卡图标的父节点和关卡预制根据实际需要赋值
    -- local parent = base.arena
    -- local prefab = base.levelPrefab
    local num = table.tablelen(map)--关卡数量
    for i=1,num do
        local data = map[i]
        local obj = utils.addchild(parent , prefab)--实例化物体并制定父节点
        local x = data.coordinate[1]
        local y = data.coordinate[2]
        obj.transform.localPosition = Vector3.New(x , y , 0)
        SetActive(obj , true)
    end
end

接着需要根据关卡图标的位置和相连关系,生成通路。但是在此之前,我们需要将配置信息转化为通路信息:例如:x1,y1,x2,y2这种格式,代表(x1,y1)与(x2,y2)之间有通路。

local function GetRoadPos()
    local allRoadPos = 
    for i,v in ipairs(map) do
        local pos1 = v.coordinate
        for i,v in ipairs(v.link) do
            local pos2 = map[v].coordinate
            local pos = pos1,pos2
            table.insert(base.allRoadPos,pos)
        end
    end
    printlog(allRoadPos,"allRoadPos ===lxlxlxl")
    return allRoadPos
end

运行发现,同一通路可能会存在多条相同的路径,这肯定是不符合我们需求的,因此我们需要将相同的路径唯一化处理:即需要再插入新路径前判断该路径是否已存在即可。

local function CheckExitRoad(pos,allRoadPos)
    for i,v in ipairs(allRoadPos) do
        if (pos[1] == v[1] and pos[2] == v[2]) or (pos[2] == v[1] and pos[1] == v[2]) then
            return true
        end
    end
    return false
end

local function GetRoadPos()
    local allRoadPos = 
    for i,v in ipairs(map) do
        local pos1 = v.coordinate
        for i,v in ipairs(v.link) do
            local pos2 = map[v].coordinate
            local pos = pos1,pos2
            local bol = base.CheckExitRoad(pos,allRoadPos)
            if not bol then
                table.insert(base.allRoadPos,pos)
            end
        end
    end
    printlog(allRoadPos,"allRoadPos ===lxlxlxl")
    return allRoadPos
end

有了路径数据,要生成路径就很简单了,直接根据坐标信息在指定位置实例化预制。需要注意的是:只关心生成位置是远远不够的,我们还需要根据两点的坐标计算出两点的距离,从而控制路径的长短。
求两点间距离,lua已经为我们封装好了:

local distance = Vector2.Distance(endPos , startPos)

有了位置长短,还需要控制路径的旋转角度,只有合适的旋转角度才能在视觉上构成真正的通路。
旋转角度的求解如下:

local rorationZ = math.atan2((x2-x1),(y2-y1))*180/math.pi-90--求旋转角度
local rotation = obj.transform.localRotation
obj.transform.localRotation = Quaternion.Euler(rotation.x,rotation.y,-rorationZ)

既然思路我们知道了,只需要闭上眼睛,把代码写出来了:

local function ShowRoad()
    --关卡路径的父节点和关卡路径预制,根据实际需要赋值
    -- local parent = base.arena
    -- local prefab = base.roadPrefab
    local allRoadPos = GetRoadPos(map)
    local num = table.tablelen(allRoadPos)
    for i=1,num do
        local pos = allRoadPos[i]
        local pos1 = pos[1]
        local pos2 = pos[2]
        local x1 = pos1[1]
        local x2 = pos2[1]
        local y1 = pos1[2]
        local y2 = pos2[2]
        obj.transform.localPosition = Vector2.New(x1, y1);
        local obj = utils.addchild(base.arena , base.roadPrefab)
        local rect = obj:GetComponent("RectTransform")
        local startPos = Vector3.New(x1, y1,0);
        local endPos = Vector3.New(x2, y2,0);
        rect.pivot = Vector2.New(0, 0.5);
        local distance = Vector2.Distance(endPos , startPos)
        rect.sizeDelta = Vector2.New(distance, rect.sizeDelta.y);

        local rorationZ = math.atan2((x2-x1),(y2-y1))*180/math.pi-90--求旋转角度
        local rotation = obj.transform.localRotation
        obj.transform.localRotation = Quaternion.Euler(rotation.x,rotation.y,-rorationZ)
        SetActive(obj , true)
    end
end

运行效果如下:

尝试2

仔细看看这个网格图,貌似在哪见过哪儿见过,努力一回忆,这不就是大学求最短路径的那种图嘛。


毕竟我也是上过学的,貌似是使用Dijkstra算法算法求最短路径。确认过眼神,你是对的算法。

--创建Dijkstra算法权中路径表
local function CreateEdges()
    local edges = 
    for i,v in ipairs(map) do
        local pos1 = v.coordinate[1]*10+v.coordinate[2]
        edges[pos1] = 
        for a,b in ipairs(v.link) do
            local coordinate1 = v.coordinate
            local coordinate2 = base.node_coordinate_proto[b].coordinate
            local x = coordinate2[1] - coordinate1[1]
            local y = coordinate2[2] - coordinate1[2]
            local pos2 = coordinate2[1]*10+coordinate2[2]
            edges[pos1][pos2] = math.sqrt(x * x + y * y) 
        end
    end
    printlog(edges,"edges =??????????")
    return edges
end

运行发现,对于下图33——>32的路径,不存在32——>33的路径,这样写出来的程序肯定是有bug的。因为这个路径表的路径是带有方向的,即a与b之间有一条通路,a可以通往b,但b不可以通往a,这肯定是我们不愿意看到的,因此,我们还需要加入逆向路径:即当有a——>b路径时,我们需要判断是否存在b——>a的路径,不存在时需要手动添加。

local function CreateEdges()
    local edges = 
    for i,v in ipairs(map) do
        local pos1 = v.coordinate[1]*10+v.coordinate[2]
        edges[pos1] = 
        for a,b in ipairs(v.link) do
            local coordinate1 = v.coordinate
            local coordinate2 = base.node_coordinate_proto[b].coordinate
            local x = coordinate2[1] - coordinate1[1]
            local y = coordinate2[2] - coordinate1[2]
            local pos2 = coordinate2[1]*10+coordinate2[2]
            edges[pos1][pos2] = math.sqrt(x * x + y * y) 
        end
    end
    printlog(edges,"edges =??????????")
    --添加逆向路径
    for k,v in pairs(edges) do
        for a,b in pairs(v) do
            local pos = a
            edges[pos] = edges[pos] or 
            local isExit = false
            for n,m in pairs(edges[pos]) do
                if n == k then
                    --已经有逆向路径
                    isExit = true
                    break
                end
            end
            if not isExit then
                edges[pos][k] = b
            end
        end
    end
    printlog(edges,"edges =添加逆向路径后的输出??????????")
    return edges
end

使用Dijkstra算法:

local coordinate = map[1].coordinate
currPos = coordinate[1]*10+coordinate[2]

local function GetShortPath(_x,_y,_finishfunc)
    local edges = CreateEdges()
    local targetPos = _x*10+_y
    local path = dijkstra.StartDijkstra(currPos,targetPos,edges)
    printlog(path,"最短路径???")
    if path == nil then
        _finishfunc(false)
        return
    else
        _finishfunc(true)
        currPos = targetPos
    end
end

local targetPos = Vector2.New(4,6)--目标位置,假设是(4,6)点,实际应用中以鼠标点击位置为准
GetShortPath(targetPos.x,targetPos.y,function (_bool)
    if _bool then
        --找到最短路径,可以前往目的地
    else
        --没有通路,不能前往目的地
    end
end)

算法测试通过之后,为了让肉眼清楚的看到路径,我们需要放上一个小人,在路上走动。
首先实例化小人:

--prefab是将要实例化的对象parent是挂载的父节点
local player = GameObject.Instantiate(prefab, parent.transform)

根据需要到达的坐标,移动小人

function chapterLevel.MoveToPos(path,idx,len)
    local pos = path[idx]
    local y = pos%10
    local x = (pos-y)/10
    local target = Vector3.New(x*100,y*100,0)
    local mypos = player.transform.localPosition
    if target.x == mypos.x and target.y == mypos.y then
        return
    end
    ChangeDir(target)
    local distance = Vector3.Distance(target , player.transform.localPosition)
    local time = distance/400
    playerAnim:SetBool("run", true)
    player.transform:DOLocalMove(target, time):SetEase(Ease.Linear):OnComplete(
        function ()
            if idx<=1 then
                playerAnim:SetBool("idle", false)
            else
                base.MoveToPos(path,idx-1,len)--递归执行两点之间的移动
            end
        end
    ) 
end

GetShortPath(coordinate[1],coordinate[2],function (_bool,path)
    if _bool then
        local len = table.tablelen(path)
        base.MoveToPos(path,len-1,len)
    end
end)

为了让小人看上去逼真一点,我们需要给他加上动画,根据需要,加上跑步和待机动画

local playerAnim = utils.getcom(player, "Animator")--获取动画组件

当需要移动的时候执行:

playerAnim:SetBool("run", true)

移动结束执行:

base.playerAnim:SetBool("idle", true)

因为人物移动时,按照常理来说都是面朝移动方向的,所以我们还需要加上人物转向的代码:

local function GetAngle(from , to, anglen以上是关于unity使用Dijkstra算法实现自动寻路的主要内容,如果未能解决你的问题,请参考以下文章

unity使用Dijkstra算法实现自动寻路

unity使用Dijkstra算法实现自动寻路

Unity 实现A* 寻路算法

unity烘焙后可以自己实现寻路吗

Unity自动寻路问题

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