如何用Swift实现A*寻路算法

144
作者 星夜暮晨
2015.08.14 15:34* 字数 5973

原文 Realm Tutorial - How To Implement A* Pathfinding with Swift

原文作者 Gabriel Hauber

CocoaChina 对应地址:
http://www.cocoachina.com/swift/20150814/13068.html

译者 星夜暮晨(QQ:412027805)

2015年8月14日


2015.08.03更新:在iOS 9当中,苹果加入了一个包含寻路API的GameplayKit新框架。本篇教程并没有对其进行讲解,我们将使用除GameplayKit之外的技术来实现寻路。

提示:本篇教程由Gabriel Hauber改编自Johann Fradj撰写的原始Cocos2D版本,并且将使用到Swift语言和Sprite Kit技术。

在本篇教程当中,我们会学习如何在一个简单的Sprite Kit游戏当中,添加A*寻路算法。A*算法用来计算两点之间的可通行路径,这在2D砖块地图(tile-based)游戏中非常有用(参考“魔塔”)。本篇教程将以CatMaze游戏作为例程。


在Spirte Kit游戏中添加A*寻路算法

如果您还不了解A*算法的话,那么您应当先阅读A*寻路算法介绍这篇文章,以便对该算法的工作原理有个大概的了解。本篇教程将涉及到如何向一个已有的Sprite Kit游戏中实现该算法。

要继续本篇教程的话,那么我建议您需要掌握Sprite Kit框架的相关知识。如果您对其不熟悉的话,可以参阅我们的SpriteKit教程,以帮助您理论结合实际,制作一个真实的游戏出来!

让我们开始吧!

首先您需要下载我们的起始项目,这是一个基于SpriteKit的小游戏,并且能够在iOS和OS X平台上运行。您可以选择一个您喜欢的平台来编译并运行该项目,如果您运行的是OS X版本的话,那么您可以见到如下图所示的游戏界面:


Cat Maze:OS X游戏界面

在这个游戏当中,您将扮演一只“盗侠猫咪”,然后逃出被狗狗守卫着的地牢。如果猫咪被狗狗抓住了,那么猫咪就会被狗狗吃掉,除非您能够给它一只骨头!您需要以正确的路线穿过地牢,这样才能够得到足够的骨头,然后通过狗狗的守卫,逃出地牢!

要注意,猫咪只能够上下左右移动,不能沿对角线移动,并且只能一格一格地移动。可行走的格子是“地面”,不可行走的格子则是“墙壁”。关于游戏的更多玩法,可以参考“魔塔”。

尽量多体验一下该游戏,看看您是否能够逃出地牢~通过单击,猫咪将会沿该方向跳动一格。(OS X版本您也可以使用方向键来控制移动)。

Cat Maze和A*算法概览

在本教程中,我们将会通过实现A*算法,从而让猫咪能够自行寻路,通过识别您单击的位置,然后寻找一个最佳的路径前往该位置,就如同大多数RPG游戏一样。

打开Cat.swift文件,然后通览一遍moveToward(_:)方法。我们可以看到它会根据猫咪当前位置以及手势单击的位置判断出方向,然后再调用moveInDirection(_:)方法完成移动。

我们之后将会修改这个方法,从而不再只是一格一格地移动,而是直接让猫咪找到最佳路径,然后自行前往该位置。

首先,用以下代码替换掉moveToward(_:)方法:

func moveToward(target: CGPoint) {
  let toTileCoord = gameScene.tileMap.tileCoordForPosition(target)
  moveTo(toTileCoord)
}

编译并运行游戏,然后试着移动一下看看,您会发现猫咪将会直接传送到选中的位置。呃,其实这就是最终效果了,点击某个位置,然后猫咪实际上是会前往该位置的,只不过现在没有了中途过程罢了。

实际的移动算法是moveTo(_:),我们将会在这个方法当中使用A*算法来计算最短路径,然后让猫咪沿该路径移动。

复用寻路方法类

实际上,无论是猫咪在满是狗狗的地牢中寻路,还是勇士在满是怪物的魔塔中寻路,它们都是用的同一套A*寻路算法。因此,我们将创建一个寻路方法类,并且该类可以在别的项目中复用!

寻路算法需要从游戏当中知晓以下信息:

  • 给定一个位置,该位置周边的哪些位置是可行走的?

  • 在两个位置之间移动需要耗费何种代价?

在项目导航器中,右键单击Shared组,然后选择New File...选项。在弹出的模板选择对话框中,选择iOS\Source\Swift File,然后单击Next。将该文件命名为AStarPathfinder,然后保存在Shared目录当中。确保CatMazeCatMaze Mac对象都被选中,然后单击Create完成创建。

向刚刚创建的文件中添加以下代码:

protocol PathfinderDataSource: NSObjectProtocol {
  func walkableAdjacentTilesCoordsForTileCoord(tileCoord: TileCoord) -> [TileCoord]
  func costToMoveFromTileCoord(fromTileCoord: TileCoord, toAdjacentTileCoord toTileCoord: TileCoord) -> Int
}

/** 基于A*算法的寻路方法类,以能够在两点之间寻找最短路径*/
class AStarPathfinder {
  weak var dataSource: PathfinderDataSource?

  func shortestPathFromTileCoord(fromTileCoord: TileCoord, toTileCoord: TileCoord) -> [TileCoord]? {
    // TODO: 现在是立即移动到目标位置
    return [toTileCoord]
  }
}

PathfinderDataSource协议描述了上面列出的两个条件信息,即可通行的临近区块,以及通行代价(比如说能量、行动点数之类的)。这些信息将会被AStarPathfinder使用,我们随后就会开始完成算法实现。

使用寻路方法类

为了使用该寻路方法类,Cat对象需要创建该类的实例,但是哪一个类作为寻路方法类的数据源更好呢?

您有两个选择:

  1. 使用GameScene类。这个类知道地图的格局,因此它用来提供诸如通行代价以及可通行区块之类的信息是非常好的选择。
  2. 使用Cat类,但为何要选这货呢?试想,如果一个游戏有多个可移动的角色,每个角色都有自己移动的规则。比如说,“鬼魂”就可以实现穿墙的效果,但是它耗费的代价也就更大。这样的话,就必须每个角色都要实现寻路方法类,而使用第一种方法就很难简单实现这个区别。

因此,为了保证良好的设计,我们选择第二种方法😄。

打开Cat.swift文件,然后在文件底部添加以下类扩展代码,以便让其实现PathfinderDataSource协议。

extension Cat: PathfinderDataSource {
  func walkableAdjacentTilesCoordsForTileCoord(tileCoord: TileCoord) -> [TileCoord] {
    let adjacentTiles = [tileCoord.top, tileCoord.left, tileCoord.bottom, tileCoord.right]
    return adjacentTiles.filter { self.gameScene.isWalkableTileForTileCoord($0) }
  }

  func costToMoveFromTileCoord(fromTileCoord: TileCoord, toAdjacentTileCoord toTileCoord: TileCoord) -> Int {
    return 1
  }
}

正如您所见,寻找可通行的区块是非常简单的一件事:建立一个数组,里面包含了给定区块的邻近区块位置,接着使用filter函数来寻找可通行的区块。

由于角色是无法沿对角线移动的,并且地面只有可通行和不可通行两种状态,并且每种状态所耗费的代价都是一样的。或许在别的游戏中,斜对角线移动可能会耗费更多的代价,或者地面状态还包括了山坡、冰面、沼泽等等多种状态,那么可能就需要进行更详细的判断。

现在,我们已经成功实现了寻路方法类的数据源,是时候来创建寻路方法类的实例了。在Cat类中添加以下属性:

let pathfinder = AStarPathfinder()
var shortestPath: [TileCoord]?

在构造方法中,在super.init()语句下立即设定寻路方法类的数据源为cat类本身。

pathfinder.dataSource = self

moveTo(_:)方法中,找到如下两句代码,这是用来更新猫咪位置和状态的语句:

position = gameScene.tileMap.positionForTileCoord(toTileCoord)
updateState()

将这两条语句替换为以下语句:

shortestPath = pathfinder.shortestPathFromTileCoord(fromTileCoord, toTileCoord: toTileCoord)

一旦我们完成了寻路算法,该属性会存储从点A前往点B的步数列表。

创建ShortestPathStep类

为了使用A*算法计算最短路径,对于路径的每步来说,我们需要知道:

  • 位置
  • F, G, H(F = G + H)
  • 上一步(这样我们就能够根据路径长度回溯到起点)

我们会在一个名为ShortestPathStep的私有类中捕获这些信息。

AStarPathfinder.swift文件的顶部添加以下代码:

/**在计算路径时候使用的单元:步,只由A*路径算法使用*/
private class ShortestPathStep: Hashable {
  let position: TileCoord
  var parent: ShortestPathStep?

  var gScore = 0
  var hScore = 0
  var fScore: Int {
    return gScore + hScore
  }

  var hashValue: Int {
    return position.col.hashValue + position.row.hashValue
  }

  init(position: TileCoord) {
    self.position = position
  }

  func setParent(parent: ShortestPathStep, withMoveCost moveCost: Int) {
    // G值 = 上一步的G值 + 上一步移动到该步的花费
    self.parent = parent
    self.gScore = parent.gScore + moveCost
  }
}

private func ==(lhs: ShortestPathStep, rhs: ShortestPathStep) -> Bool {
  return lhs.position == rhs.position
}

extension ShortestPathStep: Printable {
  var description: String {
    return "pos=\(position) g=\(gScore) h=\(hScore) f=\(fScore)"
  }
}

正如您所见,这个类十分简单,所做的工作也清晰明了,它捕获了以下信息:

  • 步的位置
  • G值(从起点到该步所在位置的总花费)
  • H值(从当前位置到终点的预计区块数)
  • 上一步的位置
  • F值(G + H,即该区块的值)

这个类还实现了Equatable协议,这样就可以通过其位置来判断两步是否相等,而无需关注其G和H值。

最后,我们还实现了Printable协议,提供了一个友好的调试信息输出。

实现A* 算法

好的,现在我们的前期准备工作已经完成了,是时候来计算最优路径了!首先,我们在AStarPathfinder类中添加以下辅助方法:

private func insertStep(step: ShortestPathStep, inout inOpenSteps openSteps: [ShortestPathStep]) {
  openSteps.append(step)
  openSteps.sort { $0.fScore <= $1.fScore }
}

func hScoreFromCoord(fromCoord: TileCoord, toCoord: TileCoord) -> Int {
  return abs(toCoord.col - fromCoord.col) + abs(toCoord.row - fromCoord.row)
}

第一个方法insertStep(_:inOpenSteps:)根据F值顺序,在openSteps列表中的合适位置插入ShortestPathStep。注意它将会修改传入的数组,因为该参数是一个inout参数。

第二个方法将会根据曼哈顿(亦称“城市街区”)距离计算方法来计算区块的H值。具体做法是计算从当前位置移动到目标位置,所需的水平和竖直距离总和,这个算法将忽略路径之间障碍物。

依赖这些辅助方法,我们就可以轻松地实现了寻路算法了。

删除shortestPathFromTileCoord(_:toTileCoord:)方法内部的TODO调用,然后将其替换成以下语句:

// 1
if self.dataSource == nil {
  return nil
}
let dataSource = self.dataSource!

// 2
var closedSteps = Set<ShortestPathStep>()
var openSteps = [ShortestPathStep(position: fromTileCoord)]

while !openSteps.isEmpty {
  // 3
  let currentStep = openSteps.removeAtIndex(0)
  closedSteps.insert(currentStep)

  // 4
  if currentStep.position == toTileCoord {
    println("PATH FOUND : ")
    var step: ShortestPathStep? = currentStep
    while step != nil {
      println(step!)
      step = step!.parent
    }
    return []
  }

  // 5
  let adjacentTiles = dataSource.walkableAdjacentTilesCoordsForTileCoord(currentStep.position)
  for tile in adjacentTiles {
    // 6
    let step = ShortestPathStep(position: tile)
    if closedSteps.contains(step) {
      continue
    }
    let moveCost = dataSource.costToMoveFromTileCoord(currentStep.position, toAdjacentTileCoord: step.position)

    if let existingIndex = find(openSteps, step) {
      // 7
      let step = openSteps[existingIndex]

      if currentStep.gScore + moveCost < step.gScore {
        step.setParent(currentStep, withMoveCost: moveCost)

        openSteps.removeAtIndex(existingIndex)
        insertStep(step, inOpenSteps: &openSteps)
      }

    } else {
      // 8
      step.setParent(currentStep, withMoveCost: moveCost)
      step.hScore = hScoreFromCoord(step.position, toCoord: toTileCoord)

      insertStep(step, inOpenSteps: &openSteps)
    }
  }

}

return nil

这个方法非常重要,因此我们将分段对其讲解:

  1. 如果没有可用的数据源,那么我们可以尽早跳出这个方法。如果有的话,我们可以建立一个本地变量来对其进行操作。

  2. 建立一个数据结构来记录步。openSteps列表将从初始位置开始。

  3. 从openSteps列表中移除最小F值的步,然后将其添加到closedSteps当中。因为列表是顺序的,因此第一步永远都是最小F值的步。

  4. 如果当前步所在位置是目标位置的话,那么就结束计算。现在,我们在控制台上将这个路径输出。

  5. 获取当前位置附近的所有可通行的区块位置信息,然后开始对其进行遍历。

  6. 获取相应步,然后检查其是否存在closeSteps列表中,如果没存在的话,那么计算其移动花费。

  7. 如果该步位于openSteps当中,那么立刻获取它。如果当前步以及移动花费优于原先步,那么将该步的上一步替换为当前步。

  8. 如果当前步不在openSteps中,计算其H值并相加。

编译并运行,然后查看一下结果!试着点击如下图所示的屏幕位置:


然后就可以在控制台中看到以下输出:

pos=[col=22 row=3] g=9 h=0 f=9
pos=[col=21 row=3] g=8 h=1 f=9
pos=[col=20 row=3] g=7 h=2 f=9
pos=[col=20 row=2] g=6 h=3 f=9
pos=[col=20 row=1] g=5 h=4 f=9
pos=[col=21 row=1] g=4 h=3 f=7
pos=[col=22 row=1] g=3 h=2 f=5
pos=[col=23 row=1] g=2 h=3 f=5
pos=[col=24 row=1] g=1 h=4 f=5
pos=[col=24 row=0] g=0 h=0 f=0

要记住这个路径是反向建立的,因此我们应当从下往上读取,从而得到算法选择了哪一条路径。请试着和游戏界面的区块进行比较确认,您会发现这真的是最短的移动路径!

逆转路径

好的现在我们得到了路径,现在我们就需要让猫咪循迹而行了。

打开Cat.swift文件,然后找到moveTo(_:)方法。在方法底部添加以下代码,就在设置shortestPath的下方:

if let shortestPath = shortestPath {
  for tileCoord in shortestPath {
    println("Step: \(tileCoord)")
  }
}

实际上到目前为止,寻路方法类并未返回路径,因此切换到AStarPathfinder.swift文件。记住,当算法结束的时候,我们拥有的路径是从终点到起点的路径。因此我们需要将其反转,然后使用TileCoords数组返回给调用者,而不是使用ShortestPathSteps数组。

AStarPathfinder类中添加以下辅助方法:

private func convertStepsToShortestPath(lastStep: ShortestPathStep) -> [TileCoord] {
  var shortestPath = [TileCoord]()
  var currentStep = lastStep
  while let parent = currentStep.parent { // 如果上一步为空,那么就表明其是起始步,因此我们不需要包含它
    shortestPath.insert(currentStep.position, atIndex: 0)
    currentStep = parent
  }
  return shortestPath
}

我们通过在数组顶端插入每一步的上一步来实现数组的反转,直到抵达其起始步为止。

shortestPathFromTileCoord(_:toTileCoord:)方法中,找到if currentStep.position == toTileCoord {声明中输出路径的代码块,将其替换为以下代码:

return convertStepsToShortestPath(currentStep)

这样就会调用辅助方法从而以恰当的次序来放置步,然后返回这条新路径。

编译并运行,如果您尝试移动猫咪,您就会看到类似的输出:

Step: [col=24 row=1]
Step: [col=23 row=1]
Step: [col=22 row=1]
Step: [col=21 row=1]
Step: [col=20 row=1]
Step: [col=20 row=2]
Step: [col=20 row=3]
Step: [col=21 row=3]
Step: [col=22 row=3]

非常棒,现在我们已经有了正常次序排列的路径,并且存放在了一个数组当中,方便我们使用。

循迹而行

最后一件事,就是遍历shortestPath数组,让猫咪循迹而行。

Cat类中添加以下代码:

func popStepAndAnimate() {
  if shortestPath == nil || shortestPath!.isEmpty {
    // 结束移动,停止动画,重设猫咪当前状态(脸朝下)
    removeActionForKey("catWalk")
    texture = SKTexture(imageNamed: "CatDown1")
    return
  }

  // 获取要移动的下一步,然后将其从路径中移除
  let nextTileCoord = shortestPath!.removeAtIndex(0)
  println(nextTileCoord)
  // 为了播放合适的动画,因此要获得当前猫咪面向的方向
  let currentTileCoord = gameScene.tileMap.tileCoordForPosition(position)

  // 确保猫咪在移动过程中面朝正确方向
  let diff = nextTileCoord - currentTileCoord
  if abs(diff.col) > abs(diff.row) {
    if diff.col > 0 {
      runAnimation(facingRightAnimation, withKey: "catWalk")
    } else {
      runAnimation(facingLeftAnimation, withKey: "catWalk")
    }
  } else {
    if diff.row > 0 {
      runAnimation(facingForwardAnimation, withKey: "catWalk")
    } else {
      runAnimation(facingBackAnimation, withKey: "catWalk")
    }
  }

  runAction(SKAction.moveTo(gameScene.tileMap.positionForTileCoord(nextTileCoord), duration: 0.4), completion: {
    let gameOver = self.updateState()
    if !gameOver {
      self.popStepAndAnimate()
    }
  })
}

这个方法从数组中弹出一步,然后根据该步所在方向让猫咪播放移动的动画。由于猫咪本身也有行走动画,因此该方法需要酌情启动和停止,以确保猫咪面向正确的方向。

在方法的末尾,即runAction(_:duration:completion:)调用中,我们需要更新游戏的状态,以检查是否允许通过狗狗,是否捡起了骨头等等方法,并且如果路径中还有下一步的话,就继续调用popStepAndAnimate()方法完成行走动画的播放。

最后,更改moveToward(_:)中的代码,调用popStepAndAnimate()方法而不是输出步:

if let shortestPath = shortestPath {
  popStepAndAnimate()
}

编译并运行,lan后:


猫咪能够自行前往您手指指向的区域,捡起骨头然后征服险恶的狗狗!

注意:在游戏过程中,如果您在猫咪行动过程中点击了一个新的位置,那么会导致猫咪的移动路径变得十分诡异。这是因为当前的移动路径被终止了,产生了一条全新的移动路径。本教程不再花精力阐述该问题如何解决,您可以下载本教程的最终版本,查看这个问题是如何解决的:重点观察Cat.swift中的currentStepActionpendingMove方法。

好的,现在我们成功实现了A* 寻路方法!是不是有点小激动呢?

番外:对角线移动

如果我们想让猫咪沿对角线移动的话,那怎么办呢?

我们只需要更新以下两个方法即可:

  • walkableAdjacentTilesCoordForTileCoord(_:):其中要包括对角线区块

  • costToMoveFromTileCoord(_:toAdjacentTileCoord:):其中要计算对角线移动的相应移动花费。

那么如何计算对角线移动的花费呢?这实际上通过一些简单的数学运算就可以完成了!

猫咪是从一个区块的中心移动到另一个区块的中心,并且由于区块是正方形,因此我们假设下图中三角形的三条边分别是A、B、C:


根据勾股定理,C² = A² + B²,因此:

C = √(A² + B²)
其中 A = B = 1 (和G值相等,即从一个区块移动到另一个区块的花费)
C = √(2)
C ≈ 1.41

因此,我们可以得到,沿C边移动的花费约是1.41,而沿A、B边移动的花费则是2,因此我们可以得出结论:对角线移动优于邻近区块移动!

不过,使用整数运算比浮点数效率更高,因此我们不用浮点数来表示对角线移动的花费,我们只需给花费乘以10即可。也就是说,水平和垂直方向移动花费10点,而对角线移动则是花费14点。

是时候来动动手了!首先替换Cat.swift中的costToMoveFromTileCoord(_:toAdjacentTileCoord:)方法:

func costToMoveFromTileCoord(fromTileCoord: TileCoord, toAdjacentTileCoord toTileCoord: TileCoord) -> Int {
  return (fromTileCoord.col != toTileCoord.col) && (fromTileCoord.row != toTileCoord.row) ? 14 : 10
}

为了给可通行的区块中添加对角线区块,因此我们还需要在TileMap.swift文件中的TileCoord类中添加一些辅助属性:

/** 左上角坐标 */
var topLeft: TileCoord {
  return TileCoord(col: col - 1, row: row - 1)
}
/** 右上角坐标 */
var topRight: TileCoord {
  return TileCoord(col: col + 1, row: row - 1)
}
/** 左下角坐标 */
var bottomLeft: TileCoord {
  return TileCoord(col: col - 1, row: row + 1)
}
/** 右下角坐标 */
var bottomRight: TileCoord {
  return TileCoord(col: col + 1, row: row + 1)
}

回到Cat.swift,修改walkableAdjacentTilesForTileCoord(_:)来返回对角线的可通行区域:

func walkableAdjacentTilesCoordsForTileCoord(tileCoord: TileCoord) -> [TileCoord] {
  var canMoveUp = gameScene.isWalkableTileForTileCoord(tileCoord.top)
  var canMoveLeft = gameScene.isWalkableTileForTileCoord(tileCoord.left)
  var canMoveDown = gameScene.isWalkableTileForTileCoord(tileCoord.bottom)
  var canMoveRight = gameScene.isWalkableTileForTileCoord(tileCoord.right)

  var walkableCoords = [TileCoord]()

  if canMoveUp {
    walkableCoords.append(tileCoord.top)
  }
  if canMoveLeft {
    walkableCoords.append(tileCoord.left)
  }
  if canMoveDown {
    walkableCoords.append(tileCoord.bottom)
  }
  if canMoveRight {
    walkableCoords.append(tileCoord.right)
  }

  // 对角线移动
  if canMoveUp && canMoveLeft && gameScene.isWalkableTileForTileCoord(tileCoord.topLeft) {
    walkableCoords.append(tileCoord.topLeft)
  }
  if canMoveDown && canMoveLeft && gameScene.isWalkableTileForTileCoord(tileCoord.bottomLeft) {
    walkableCoords.append(tileCoord.bottomLeft)
  }
  if canMoveUp && canMoveRight && gameScene.isWalkableTileForTileCoord(tileCoord.topRight) {
    walkableCoords.append(tileCoord.topRight)
  }
  if canMoveDown && canMoveRight && gameScene.isWalkableTileForTileCoord(tileCoord.bottomRight) {
    walkableCoords.append(tileCoord.bottomRight)
  }

  return walkableCoords
}

貌似添加对角线区块的方法比水平竖直移动来得更为复杂,闹哪样?

这是因为对角线移动必须要满足一定的规则,比如说猫咪要向左下角移动,这就要求猫咪能够向左移动以及向下移动。这样能够放置猫咪穿墙而过,就如同下图所示:


在图中:

  • O:起点
  • T:顶部
  • B:底部
  • L:左侧
  • R:右侧
  • TL:左上角
  • ...

就以上图左上角的猫咪为示范,我们举一个例子。

猫咪想要从起点(O)前往左下角,如果在左侧或者其底部有墙存在,那么斜对角移动就会导致穿墙而过。因此,我们必须要求左侧和底部都是空旷的,不能够存在墙以及任何障碍物。

提示:您可以通过更新costToMoveFromTileCoord(_:toAdjacentTileCoord:)方法来模拟不同的区域类型,这样可以增加多种地形。降低该地形的花费会让猫咪更乐意走该地形,增加该地形的花费会让猫咪尽量避免走该地形。

编译并运行项目,查看猫咪现在的行动,很显然,猫咪可以对角移动了。

挑战

那么如何实现让猫咪尽可能避免通过有狗狗存在的路线呢?这样就可以避免手动控制猫咪避开狗狗去拿骨头。您可以试一试~

首先,要注意的一点是,我们不能够标记狗狗所在的区域为“不可通行”的区域,这样猫咪就永远无法通过该区域了!相反,我们需要增加狗狗所在区域的花费。

例如,我们可以将有狗狗存在的区块花费更多,比如说:

func costToMoveFromTileCoord(fromTileCoord: TileCoord, toAdjacentTileCoord toTileCoord: TileCoord) -> Int {
  let baseCost = (fromTileCoord.col != toTileCoord.col) &&
                 (fromTileCoord.row != toTileCoord.row) ? 14 : 10
  return baseCost * (gameScene.isDogAtTileCoord(toTileCoord) ? 10 : 1)
}

接下来该何去何从?

您可以下载本教程的最终项目,当中包含了对角线移动以及自动避开狗狗的行为。

非常好,现在您已经大概了解了一些A*算法的基础知识,并且知晓了如何实现它。您现在应该可以:

  • 在自己的游戏当中实现A*算法
  • 对其进行修改,增加地形,给多名角色增加此算法等等

要了解关于A算法的更多知识,您可以前往[Amit’s A Pages](http://theory.stanford.edu/~amitp/GameProgramming/)。

如果您对本教程有任何问题和评论,欢迎在本文下方加以评论!

 技术文档翻译