A星寻路 - lua实现

A星的大致流程:

每走下一步前,评估所有可走下一步的移动量,然后从中选择移动量最小步骤;不断重复这个过程,直到到达目的地。

 

图解

下图中,0为可行走,1为障碍,灰色为起点,橘色为目标点

image

 

1、(1, 6)可达的格子为(2, 6)和(1, 5)

(2, 6)到起点的移动量g=1, 到终点的移动量h=4, 放入OpenList

(1, 5)到起点的移动量g=1, 到终点的移动量h=4, 放入OpenList

OpenList: (2, 6), (1, 5)

CloseList: 空

image

 

2、从OpenList取出(g+h)最小的格子(2, 6),放入CloseList

(2, 6)可达的格子为(3, 6)

(3, 6)到起点的移动量g=2, 到终点的移动量h=3, 放入OpenList

OpenList: (1, 5), (3, 6)

CloseList: (2, 6)

image

 

3、从OpenList取出(g+h)最小的格子(1, 5),放入CloseList

(1, 5)可达的格子为(1, 4)

(1, 4)到起点的移动量g=2, 到终点的移动量h=5, 放入OpenList

OpenList: (3, 6), (1, 4)

CloseList: (2, 6), (1, 5)

image

 

4、从OpenList取出(g+h)最小的格子(3, 6),放入CloseList

(3, 6)可达的格子为(4, 6), (3, 5)

(4, 6)到起点的移动量g=3, 到终点的移动量h=2, 放入OpenList

(3, 5)到起点的移动量g=3, 到终点的移动量h=2, 放入OpenList

OpenList: (1, 4), (4, 6), (3, 5)

CloseList: (2, 6), (1, 5), (3, 6)

image

 

4、从OpenList取出(g+h)最小的格子(4, 6),放入CloseList

(4, 6)可达的格子为(5, 6), (4, 5)

(5, 6)到起点的移动量g=4, 到终点的移动量h=1, 放入OpenList

(4, 5)到起点的移动量g=4, 到终点的移动量h=1, 放入OpenList

OpenList: (1, 4), (3, 5), (5, 6), (4, 5)

CloseList: (2, 6), (1, 5), (3, 6), (4, 6)

image

 

5、从OpenList取出(g+h)最小的格子(3, 5),放入CloseList

(3, 5)可达的格子为(4, 5), (3, 4)

(4, 5)格子已走过, 且(3, 5)的g=3, (4, 5)的g=4, 没有更短路径, 忽略

(3, 4)到起点的移动量g=4, 到终点的移动量h=3, 放入OpenList

OpenList: (1, 4), (5, 6), (4, 5), (3, 4)

CloseList: (2, 6), (1, 5), (3, 6), (4, 6), (3, 5)

image

 

5、从OpenList取出(g+h)最小的格子(5, 6),放入CloseList

(5, 6)可达的格子为(6, 6), (5, 5)

(6, 6)到起点的移动量g=5, 到终点的移动量h=2, 放入OpenList

(5, 5)为目标点,放入CloseList

OpenList: (1, 4), (4, 5), (3, 4), (6, 6)

CloseList: (2, 6), (1, 5), (3, 6), (4, 6), (3, 5), (5, 6), (5, 5)

image

从(5, 5)往前回溯步骤得到移动路径:(5, 5) <- (5, 6) <- (4, 6) <- (3, 6) <- (2, 6) <- (1, 6)。注意:不是CloseList

 

核心代码

---2个点之间是否有最佳路径
---@return AStar.Point[]? path 没有最佳路径则返回nil
function AStar:FindPath(startCellX, startCellY, targetCellX, targetCellY)
    if startCellX == targetCellX and startCellY == targetCellY then return nil end

    if self:_CheckOutOfRange(startCellX, startCellY) then return nil end
    if self:_CheckOutOfRange(targetCellX, targetCellY) then return nil end

    self:_ResetStepInfos()

    self.m_TargetCell.x = targetCellX
    self.m_TargetCell.y = targetCellY

    self.m_OpenList = {}
    local tempStepInfo = self:GetStepInfo(startCellX, startCellY)
    tempStepInfo.used = true

    if self:_AddWalkables(tempStepInfo) then --到目标点了
        return self:_GetWalkPath(startCellX, startCellY)
    end

    repeat
        local best = self:_FindBest()
        if nil == best then
            break
        end

        if self:_AddWalkables(best) then --到目标点了
            return self:_GetWalkPath(startCellX, startCellY)
        end
    until #self.m_OpenList <= 0

    return nil
end

---检查上下左右格子, 如果可以走, 则放入open列表
---@param fromStepInfo AStar.StepInfo
---@return boolean isReachTarget 是否到达目标点
function AStar:_AddWalkables(fromStepInfo)
    local cellPos = fromStepInfo.cellPos
    --print(string.format("AddWalkables: %s, %s", cellPos.x, cellPos.y))

    local result = self:_AddWalkable(fromStepInfo, cellPos.x+1, cellPos.y)
        or self:_AddWalkable(fromStepInfo, cellPos.x, cellPos.y-1)
        or self:_AddWalkable(fromStepInfo, cellPos.x-1, cellPos.y)
        or self:_AddWalkable(fromStepInfo, cellPos.x, cellPos.y+1)
    return result
end

---检查相邻的格子, 如果可以走则放入open列表
---@param fromStepInfo AStar.StepInfo
---@return boolean isReachTarget 是否已到达目标点
function AStar:_AddWalkable(fromStepInfo, toCellX, toCellY)
    local targetCell = self.m_TargetCell

    if toCellX == targetCell.x and toCellY == targetCell.y then --到达终点
        local toStepInfo = self:GetStepInfo(toCellX, toCellY)
        toStepInfo.prevStep = fromStepInfo
        return true
    end

    if nil ~= self.m_Cells[toCellY] then
        local cell = self.m_Cells[toCellY][toCellX]
        if (self.m_CellGetter and self.m_CellGetter(cell)) or 0 == cell then
            local toStepInfo = self:GetStepInfo(toCellX, toCellY)
            if not toStepInfo.used then
                toStepInfo.g = fromStepInfo.g + 1
                toStepInfo.h = math.abs(toCellX - targetCell.x) + math.abs(toCellY - targetCell.y)
                toStepInfo.f = toStepInfo.g + toStepInfo.h
                toStepInfo.used = true
                toStepInfo.prevStep = fromStepInfo
                table.insert(self.m_OpenList, toStepInfo)
                --print(string.format("cell:(%s, %s), g:%s, h:%s", toCellX, toCellY, toStepInfo.g, toStepInfo.h))
            else
                local gTemp = fromStepInfo.g + 1
                if gTemp < toStepInfo.g then --发现更短路径
                    toStepInfo.g = gTemp
                    toStepInfo.h = math.abs(toCellX - targetCell.x) + math.abs(toCellY - targetCell.y)
                    toStepInfo.f = toStepInfo.g + toStepInfo.h
                    toStepInfo.prevStep = fromStepInfo
                end
            end
        else
            --超出范围
        end
    else
        --超出范围
    end

    return false
end

---从open列表中找一个最佳的格子
function AStar:_FindBest()
    local openList = self.m_OpenList
    local cnt = #openList
    if cnt == 0 then return end

    local best = openList[1]
    local index = 1
    for i=2,cnt do
        local stepInfo = openList[i]
        if stepInfo.f < best.f then
            best = stepInfo
            index = i
        end
    end
    table.remove(openList, index)
    return best
end

 

其余代码

---@class AStar
---@field new fun() : AStar
local AStar = {}
AStar.__index = AStar

---@class AStar.Point
---@field x integer
---@field y integer

---@class AStar.StepInfo
---@field cellPos AStar.Point
---@field g integer 起点到该点的移动量
---@field h integer 该点到终点的预估移动量
---@field f integer g+h
---@field used boolean
---@field prevStep AStar.StepInfo

function AStar.new()
    local inst = {}
    setmetatable(inst, AStar)
    inst:ctor()
    return inst
end

function AStar:ctor()
    --- 格子信息, 使用[y][x]获取, 存放格子是否可以走
    self.m_Cells = nil ---@as table<integer, table<integer, integer>>
    self.m_CellGetter = nil
    
    ---用于记录走到某个格子时的一些信息
    self.m_CachedStepInfos = nil ---@as table<integer, table<integer, AStar.StepInfo>> 

    self.m_OpenList = nil ---@as AStar.StepInfo[] 下一步可以走的格子都会加入该列表

    self.m_TargetCell = { x = 0, y = 0 } ---目标位置
end

---@param getter fun(cellX, cellY) : integer 0_可走, 1_不可走, nil_超出范围
function AStar:SetTiles(tiles, getter)
    self.m_Cells = tiles
    self.m_CellGetter = getter
    self.m_CachedStepInfos = nil
end

function AStar:_CheckOutOfRange(cellX, cellY)
    local row = self.m_Cells[cellY]
    if nil == row then return true end
    if nil == row[cellX] then return true end
    return false
end

function AStar:_ResetStepInfos()
    if nil == self.m_CachedStepInfos then return end

    for k1_RowIndex, v1_RowStepInfos in pairs(self.m_CachedStepInfos) do
        for k2_ColIndex, v2_OneStepInfo in pairs(v1_RowStepInfos) do
            v2_OneStepInfo.g = 0
            v2_OneStepInfo.h = 0
            v2_OneStepInfo.f = 0 
            v2_OneStepInfo.used = false
            v2_OneStepInfo.prevStep = nil
        end
    end
end

---@return AStar.StepInfo
function AStar:GetStepInfo(cellX, cellY)
    if nil == self.m_CachedStepInfos then
        self.m_CachedStepInfos = {}
    end
    local rowStepInfos = self.m_CachedStepInfos[cellY]
    if nil == rowStepInfos then
        rowStepInfos = {}
        self.m_CachedStepInfos[cellY] = rowStepInfos
    end

    local oneStepInfo = rowStepInfos[cellX]
    if nil == oneStepInfo then
        oneStepInfo = {
            cellPos = { x=cellX, y=cellY },
            g = 0,
            h = 0,
            f = 0,
        }
        rowStepInfos[cellX] = oneStepInfo
    end

    return oneStepInfo
end

---close列表转换为移动路径
function AStar:_GetWalkPath(startCellX, startCellY)
    local result = {}

    local tempStepInfo = self:GetStepInfo(self.m_TargetCell.x, self.m_TargetCell.y)
    while nil ~= tempStepInfo do
        local cellPos = tempStepInfo.cellPos
        if cellPos.x == startCellX and cellPos.y == startCellY then
            break
        end
        table.insert(result, 1, cellPos)
        tempStepInfo = tempStepInfo.prevStep
    end

    self.m_OpenList = nil
    return result
end

return AStar

 

测试代码

local a = Astar.new()
local tiles = {}
tiles[1] = { 0, 0, 0, 0, 0, 0, 0 }
tiles[2] = { 0, 0, 0, 1, 0, 0, 0 }
tiles[3] = { 0, 0, 0, 1, 0, 0, 0 }
tiles[4] = { 0, 0, 0, 1, 0, 0, 0 }
tiles[5] = { 0, 1, 0, 0, 0, 0, 0 }
tiles[6] = { 0, 0, 0, 0, 0, 0, 0 }
a:SetTiles(tiles)

local path = a:FindPath(1, 6, 5, 5)
if path and #path > 0 then
    for i=1,#path do
        local loc = path[i]
        print(string.format("row=%s, col=%s", loc.y, loc.x))
    end
end

 

【A星原理参考】

A星寻路算法介绍 - 莫水千流 - 博客园 (cnblogs.com)

 A星寻路算法 - 简书 (jianshu.com)

A星寻路算法 - szmtjs10 - 博客园 (cnblogs.com)

 

posted @ 2022-02-20 17:38  yanghui01  阅读(317)  评论(0)    收藏  举报