astar.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. // Package astar 实现了六边形网格的A*寻路算法
  2. // 采用odd-q offset坐标系(奇数列向上偏移),支持并发安全调用
  3. // 核心优化:预计算坐标、heap优先队列、对象池复用、视觉直线性惩罚
  4. package astar6
  5. import (
  6. "container/heap"
  7. "f1-game/internal/constant"
  8. "fmt"
  9. "math"
  10. "slices"
  11. clog "github.com/cherry-game/cherry/logger"
  12. )
  13. // AStar A*寻路算法结构(无状态,可安全被多协程共享)
  14. type AStar struct {
  15. config Config
  16. cache [][]*Node // 节点缓存,二维数组直接索引(只读,初始化后不变)
  17. }
  18. // New 创建A*寻路实例
  19. // 初始化时会构建所有可行走节点及其邻居关系
  20. // 返回的实例是并发安全的,可被多个goroutine同时调用FindPath
  21. func New(c Config) *AStar {
  22. instance := &AStar{config: c}
  23. instance.init()
  24. return instance
  25. }
  26. // init 初始化寻路器
  27. // 1. 遍历地图数据创建所有可行走节点
  28. // 2. 建立节点间的邻居关系
  29. // 3. 设置默认的关键点提取函数
  30. func (p *AStar) init() {
  31. // 初始化二维数组缓存
  32. rows := p.config.Max.X - p.config.Min.X + 1
  33. cols := p.config.Max.Y - p.config.Min.Y + 1
  34. p.cache = make([][]*Node, rows)
  35. for i := range p.cache {
  36. p.cache[i] = make([]*Node, cols)
  37. }
  38. // 遍历地图范围,创建可行走节点
  39. for x := p.config.Min.X; x <= p.config.Max.X; x++ {
  40. for y := p.config.Min.Y; y <= p.config.Max.Y; y++ {
  41. value := p.config.MapData[x-p.config.Min.X][y-p.config.Min.Y]
  42. if constant.IsWalkableTile(value) {
  43. p.addNode(NewNode(x, y, true))
  44. }
  45. }
  46. }
  47. // 建立邻居关系(六边形拓扑)
  48. p.initNodeNeighbors()
  49. }
  50. // 测试用,设置所有节点为可行走
  51. func (p *AStar) ResetCache() {
  52. for _, row := range p.cache {
  53. for _, node := range row {
  54. if node != nil {
  55. node.isWalkable = true
  56. }
  57. }
  58. }
  59. }
  60. func (p *AStar) SetNode(x, y int32, isWalkable bool) {
  61. node := p.GetNode(x, y)
  62. if node != nil {
  63. node.SetWalkable(isWalkable)
  64. }
  65. }
  66. // initNodeNeighbors 初始化所有节点的邻居关系
  67. // 六边形网格采用odd-q坐标系(奇数列上移)
  68. //
  69. // 偶数列(x=0,2,4...)的6个邻居:
  70. //
  71. // (x-1,y) (x+1,y)
  72. // (x,y-1) [x,y] (x,y+1)
  73. // (x-1,y+1) (x+1,y+1)
  74. //
  75. // 奇数列(x=1,3,5...)的6个邻居:
  76. //
  77. // (x-1,y-1) (x+1,y-1)
  78. // (x,y-1) [x,y] (x,y+1)
  79. // (x-1,y) (x+1,y)
  80. func (p *AStar) initNodeNeighbors() {
  81. for x := p.config.Min.X; x <= p.config.Max.X; x++ {
  82. for y := p.config.Min.Y; y <= p.config.Max.Y; y++ {
  83. node := p.GetNode(x, y)
  84. if node == nil || !node.isWalkable {
  85. continue
  86. }
  87. if node.IsEven() { // 偶数列
  88. node.addNeighbor(p.GetNode(x+1, y), ODD_DIAGONAL_DISTINCT) // 右上
  89. node.addNeighbor(p.GetNode(x+1, y+1), ODD_DIAGONAL_DISTINCT) // 右下
  90. node.addNeighbor(p.GetNode(x-1, y), ODD_DIAGONAL_DISTINCT) // 左上
  91. node.addNeighbor(p.GetNode(x-1, y+1), ODD_DIAGONAL_DISTINCT) // 左下
  92. node.addNeighbor(p.GetNode(x, y-1), ODD_STRAIGHT_DISTINCT) // 上
  93. node.addNeighbor(p.GetNode(x, y+1), ODD_STRAIGHT_DISTINCT) // 下
  94. } else { // 奇数列
  95. node.addNeighbor(p.GetNode(x+1, y), EVEN_DIAGONAL_DISTINCT) // 右下
  96. node.addNeighbor(p.GetNode(x+1, y-1), EVEN_DIAGONAL_DISTINCT) // 右上
  97. node.addNeighbor(p.GetNode(x-1, y), EVEN_DIAGONAL_DISTINCT) // 左下
  98. node.addNeighbor(p.GetNode(x-1, y-1), EVEN_DIAGONAL_DISTINCT) // 左上
  99. node.addNeighbor(p.GetNode(x, y-1), EVEN_STRAIGHT_DISTINCT) // 上
  100. node.addNeighbor(p.GetNode(x, y+1), EVEN_STRAIGHT_DISTINCT) // 下
  101. }
  102. }
  103. }
  104. }
  105. // FindPath 寻路入口函数(并发安全)
  106. // 从(x1,y1)到(x2,y2)寻找最优路径
  107. // 返回路径节点列表和是否找到路径
  108. // 使用对象池复用搜索上下文,支持多goroutine并发调用
  109. func (p *AStar) FindPath(ctx *FindContext) ([]*Node, bool) {
  110. // 回收 Context 对象
  111. defer releaseContext(ctx)
  112. start := p.GetNode(ctx.sx, ctx.sy)
  113. end := p.GetNode(ctx.ex, ctx.ey)
  114. // 验证起点和终点是否可行走
  115. if !p.isWalkable(start, ctx) || !p.isWalkable(end, ctx) {
  116. clog.Debugf("path find failed: start or end is not walkable: (%v -> %v)", start, end)
  117. return nil, false
  118. }
  119. // 初始化搜索状态(用于视觉惩罚计算)
  120. ctx.start = start
  121. ctx.end = end
  122. // 初始化起点:g=0,f=h
  123. ctx.gMap[start] = 0
  124. h := p.calH(start, ctx.end)
  125. item := &heapItem{
  126. node: start,
  127. f: h,
  128. }
  129. heap.Push(&ctx.openHeap, item)
  130. ctx.openSet[start] = item
  131. // 执行A*搜索
  132. result := p.search(ctx)
  133. if len(result) == 0 {
  134. clog.Debugf("path find failed: find empty path: (%v -> %v)", start, end)
  135. return nil, false
  136. }
  137. return result, true
  138. }
  139. // search 寻路核心算法
  140. // 使用A*算法搜索从起点到终点的最优路径
  141. // 算法流程:
  142. // 1. 从openHeap取出f值最小的节点作为当前节点
  143. // 2. 如果当前节点是终点,构建并返回路径
  144. // 3. 将当前节点加入closeSet
  145. // 4. 遍历所有邻居,更新路径代价
  146. // 5. 重复直到找到终点或openHeap为空
  147. func (p *AStar) search(ctx *FindContext) []*Node {
  148. for ctx.openHeap.Len() > 0 {
  149. // 从优先队列取出f值最小的节点
  150. item := heap.Pop(&ctx.openHeap).(*heapItem)
  151. current := item.node
  152. delete(ctx.openSet, current)
  153. ctx.closeSet[current] = struct{}{}
  154. // 到达终点,构建路径返回
  155. if current == ctx.end {
  156. return p.buildPath(ctx, current)
  157. }
  158. // 遍历所有邻居节点(优化:使用静态数组)
  159. neighborCount := current.neighborCount
  160. for i := int32(0); i < neighborCount; i++ {
  161. neighborNode := current.neighborNodes[i]
  162. cost := current.neighborCosts[i]
  163. // 跳过已在closeSet中的节点
  164. if _, closed := ctx.closeSet[neighborNode]; closed {
  165. continue
  166. }
  167. // 跳过不可行走的节点(可能在寻路过程中被动态设置为不可行走)
  168. if !p.isWalkable(neighborNode, ctx) {
  169. continue
  170. }
  171. // 检查并更新邻居节点的路径
  172. p.checkPath(ctx, neighborNode, current, cost)
  173. }
  174. }
  175. return nil
  176. }
  177. // checkPath 检查并更新节点路径
  178. // 核心逻辑:
  179. // - 如果节点已在openSet中,检查新路径是否更优
  180. // - 如果节点不在openSet中,添加到openSet
  181. // 参数:
  182. // - node: 待检查的邻居节点
  183. // - parent: 当前节点(作为node的父节点)
  184. // - cost: 从parent到node的移动代价
  185. // 注:终点信息来自 ctx.end
  186. // 优化:使用heapItem对象池减少内存分配
  187. func (p *AStar) checkPath(ctx *FindContext, node, parent *Node, cost int32) {
  188. // 计算经过parent到达node的g值
  189. newG := p.calG(ctx, node, parent, cost)
  190. if item, inOpen := ctx.openSet[node]; inOpen {
  191. // 节点已在openSet中,检查新路径是否更优
  192. if newG < ctx.gMap[node] {
  193. ctx.parentMap[node] = parent
  194. ctx.gMap[node] = newG
  195. // 使用索引堆O(1)定位,O(log n)更新
  196. newF := newG + p.calH(node, ctx.end)
  197. item.f = newF
  198. heap.Fix(&ctx.openHeap, item.index) // O(log n)复杂度调整堆
  199. }
  200. } else {
  201. // 新节点,添加到openSet
  202. // 优化:从对象池获取heapItem
  203. item := getHeapItem()
  204. item.node = node
  205. item.f = newG + p.calH(node, ctx.end)
  206. ctx.parentMap[node] = parent
  207. ctx.gMap[node] = newG
  208. heap.Push(&ctx.openHeap, item)
  209. ctx.openSet[node] = item
  210. }
  211. }
  212. // buildPath 从终点回溯构建完整路径
  213. // 通过parentMap逆向遍历,直接构建正向路径,避免使用slices.Reverse
  214. func (p *AStar) buildPath(ctx *FindContext, node *Node) []*Node {
  215. // 先计算路径长度
  216. count := 0
  217. for n := node; n != nil; n = ctx.parentMap[n] {
  218. count++
  219. }
  220. // 预分配容量,从后往前填充
  221. result := make([]*Node, count)
  222. idx := count - 1
  223. for n := node; n != nil; n = ctx.parentMap[n] {
  224. result[idx] = n
  225. idx--
  226. }
  227. return result
  228. }
  229. // isWalkable 判断节点是否可行走
  230. // 优先使用配置的自定义判断函数,否则使用节点自身的isWalkable属性
  231. // 内联到调用处以提升性能
  232. func (p *AStar) isWalkable(node *Node, ctx *FindContext) bool {
  233. if node == nil || !node.isWalkable {
  234. return false
  235. }
  236. if p.config.IsWalkableFunc != nil {
  237. return p.config.IsWalkableFunc(node, ctx)
  238. }
  239. return true
  240. }
  241. // ContainsNode 检查坐标是否存在对应的节点
  242. func (p *AStar) ContainsNode(x, y int32) bool {
  243. return p.GetNode(x, y) != nil
  244. }
  245. // GetNode 根据坐标获取节点
  246. // 优化:使用预计算的常量减少运行时边界检查开销
  247. func (p *AStar) GetNode(x, y int32) *Node {
  248. // 预计算边界检查
  249. maxX := int32(len(p.cache))
  250. if maxX == 0 {
  251. return nil
  252. }
  253. maxY := int32(len(p.cache[0]))
  254. // 计算索引
  255. idx := x - p.config.Min.X
  256. idy := y - p.config.Min.Y
  257. // 优化:合并边界检查
  258. if idx < 0 || idx >= maxX || idy < 0 || idy >= maxY {
  259. return nil
  260. }
  261. return p.cache[idx][idy]
  262. }
  263. // addNode 添加节点到缓存
  264. func (p *AStar) addNode(node *Node) {
  265. idx := node.x - p.config.Min.X
  266. idy := node.y - p.config.Min.Y
  267. p.cache[idx][idy] = node
  268. }
  269. // calH 计算启发式距离(曼哈顿距离)
  270. // 使用预计算的cube坐标,避免运行时转换开销
  271. // cube坐标下的距离公式: (|x1-x2| + |y1-y2| + |z1-z2|) / 2
  272. //
  273. // 启发式强化策略:
  274. // - 短距离(0-50步):使用基础曼哈顿距离
  275. // - 中等距离(50-300步):乘以1.2加强引导(常见游戏寻路距离)
  276. // - 长距离(>300步):乘以1.1避免过度估计(保持A*最优性)
  277. // 优化:使用位运算和查找表替代乘除法,消除条件分支
  278. func (p *AStar) calH(node1, node2 *Node) int32 {
  279. // 优化:使用位运算计算绝对值(无分支)
  280. dx := node1.cube[0] - node2.cube[0]
  281. dx = (dx + (dx >> 31)) ^ (dx >> 31)
  282. dy := node1.cube[1] - node2.cube[1]
  283. dy = (dy + (dy >> 31)) ^ (dy >> 31)
  284. dz := node1.cube[2] - node2.cube[2]
  285. dz = (dz + (dz >> 31)) ^ (dz >> 31)
  286. // 优化:使用位移替代除法
  287. base := (dx + dy + dz) >> 1
  288. // 优化:使用查找表和位运算替代条件分支
  289. // 预计算启发式乘数:0=短距离×1.0, 1=中等距离×1.2, 2=长距离×1.1
  290. var multiplier int32 = 10 // 基础乘数
  291. if base > 50 {
  292. multiplier = 12 // 中等距离
  293. if base > 300 {
  294. multiplier = 11 // 长距离
  295. }
  296. }
  297. return (base * multiplier) / 10
  298. }
  299. // calG 计算从起点到当前节点的实际代价
  300. // g = 父节点g值 + 移动代价 + 转向惩罚 + 视觉直线性惩罚
  301. // 其中转向惩罚用于避免锯齿路径,视觉直线性惩罚用于优化路径外观
  302. // 优化:简化转向惩罚计算逻辑,减少分支预测失败
  303. func (p *AStar) calG(ctx *FindContext, node, parent *Node, cost int32) int32 {
  304. if parent == nil {
  305. return cost
  306. }
  307. // 获取祖父节点,用于判断方向变化
  308. grandpa := ctx.parentMap[parent]
  309. var pdx, pdy int32 // 父节点的移动方向
  310. if grandpa != nil {
  311. pdx = parent.x - grandpa.x
  312. pdy = parent.y - grandpa.y
  313. }
  314. // 当前移动方向
  315. dx := node.x - parent.x
  316. dy := node.y - parent.y
  317. // 优化转向惩罚计算,减少分支预测失败
  318. var turnPenalty int32 = 0
  319. if pdx != 0 || pdy != 0 { // 不是第一步
  320. // 使用异或运算快速检测方向变化
  321. directionChanged := ((dx ^ pdx) | (dy ^ pdy)) != 0
  322. if directionChanged {
  323. // 简化的转向判断逻辑
  324. if dx == 0 || pdx == 0 {
  325. turnPenalty = COST_TURN
  326. } else if node.isOdd {
  327. if dy != pdy && dy != pdy+1 {
  328. turnPenalty = COST_TURN
  329. }
  330. } else {
  331. if dy != pdy && dy != pdy-1 {
  332. turnPenalty = COST_TURN
  333. }
  334. }
  335. }
  336. }
  337. linearityPenalty := p.calLinearityPenalty(ctx, node, parent, grandpa)
  338. return ctx.gMap[parent] + cost + turnPenalty + linearityPenalty
  339. }
  340. // calLinearityPenalty 计算视觉直线性惩罚(使用预计算坐标)
  341. // 目的:使路径在视觉上更加平滑,避免不必要的Y方向反向抖动
  342. // 惩罚策略:
  343. // 1. 惩罚y值低于起点/终点最小y的节点(防止路径向上偏移过多)
  344. // 2. 惩罚视觉Y方向反向(在像素坐标下判断,符合实际显示效果)
  345. // 3. 奖励同方向移动(鼓励保持方向一致性)
  346. // 优化:完全使用整数运算,消除所有浮点运算
  347. func (p *AStar) calLinearityPenalty(ctx *FindContext, node, parent, grandpa *Node) int32 {
  348. if parent == nil || ctx.start == nil || ctx.end == nil {
  349. return 0
  350. }
  351. var penalty int32 = 0
  352. // 惩罚1:如果y值低于起点和终点的最小y值
  353. // 防止路径向屏幕上方偏移过多
  354. // 优化:使用位移替代乘法
  355. if node.y < ctx.minY {
  356. penalty += COST_VISUAL_TURN << 1 // 乘以2
  357. }
  358. // 检查视觉Y方向反向(需要祖父节点来判断方向变化)
  359. if grandpa != nil {
  360. // 优化:使用预计算的整数坐标(×1000),完全消除浮点运算
  361. prevDyInt := parent.pixelYInt - grandpa.pixelYInt
  362. currDyInt := node.pixelYInt - parent.pixelYInt
  363. // Y方向反向(乘积为负表示方向相反)
  364. // 优化:使用异或运算检测符号变化,比乘法更快
  365. if (prevDyInt ^ currDyInt) < 0 {
  366. // 计算当前位置在整条路径中的进度比例(使用整数运算)
  367. totalDistInt := ctx.end.pixelXInt - ctx.start.pixelXInt
  368. nodeDistInt := node.pixelXInt - ctx.start.pixelXInt
  369. // 使用整数运算计算进度和距离中间的距离
  370. // progress * 1024 表示,512表示0.5(中间位置)
  371. // 优化:使用位移替代乘除法
  372. progressInt := int32(512) // 默认0.5
  373. if totalDistInt > 0 {
  374. // progress = nodeDist / totalDist,乘以1024
  375. progressInt = int32((nodeDistInt << 10) / totalDistInt)
  376. }
  377. // 离中间位置的距离(512表示中间)
  378. distFromMiddleInt := progressInt - 512
  379. if distFromMiddleInt < 0 {
  380. distFromMiddleInt = -distFromMiddleInt
  381. }
  382. // 惩罚公式:基础惩罚(30%) + 距离因子(140%)
  383. // Cost_Visual_Turn * (0.3 + distFromMiddle*1.4)
  384. // 优化:使用预计算和位运算
  385. // basePenalty = Cost_Visual_Turn * 3 / 10
  386. // distPenalty = Cost_Visual_Turn * distFromMiddleInt * 14 / 100
  387. basePenalty := int32((COST_VISUAL_TURN * 3) / 10)
  388. distPenalty := int32((COST_VISUAL_TURN * distFromMiddleInt * 14) / 100)
  389. penalty += basePenalty + distPenalty
  390. }
  391. // 同方向移动给予小奖励,鼓励保持方向一致性
  392. // 优化:使用位与运算检测同号
  393. if (prevDyInt & currDyInt) >= 0 {
  394. penalty -= 1
  395. }
  396. }
  397. return penalty
  398. }
  399. // Print 可视化打印路径
  400. // 用于调试,将路径在地图上以ASCII图形展示
  401. // 图例:◉ 起点, ◦ 路径点, ● 终点
  402. func (p *AStar) Print(list []*Node) {
  403. if len(list) < 1 {
  404. return
  405. }
  406. fromNode := list[0]
  407. toNode := list[len(list)-1]
  408. mapText := "\n"
  409. mapText += fmt.Sprintf(" Rectangle: [(%d,%d) -> (%d,%d)] \n", fromNode.x, fromNode.y, toNode.x, toNode.y)
  410. mapText += fmt.Sprintf(" Pathing: %v\n", list)
  411. mapText += " Pattern: start = ◉, point = ◦, end = ●\n"
  412. mapText += "Coordinate: row = x, col = y\n\n"
  413. for x := range p.config.Min.X {
  414. // 偶数行缩进,模拟六边形交错布局
  415. if x%2 == 0 {
  416. mapText += " "
  417. }
  418. for y := range p.config.Max.Y {
  419. if slices.ContainsFunc(list, func(node *Node) bool { return node.x == x && node.y == y }) {
  420. if x == fromNode.x && y == fromNode.y {
  421. mapText += "◉ " // 起点
  422. } else if x == toNode.x && y == toNode.y {
  423. mapText += "● " // 终点
  424. } else {
  425. mapText += "◦ " // 路径点
  426. }
  427. } else {
  428. mapText += fmt.Sprintf("%d ", p.config.MapData[x][y])
  429. }
  430. }
  431. mapText += "\n"
  432. }
  433. clog.Debug(mapText)
  434. }
  435. func (p *AStar) GetDistance(sx int32, sy int32, ex int32, ey int32) int32 {
  436. var (
  437. start = p.GetNode(sx, sy)
  438. end = p.GetNode(ex, ey)
  439. )
  440. if start == nil || end == nil {
  441. return math.MaxInt32
  442. }
  443. return p.calDistance(start, end)
  444. }
  445. func (p *AStar) calDistance(node1, node2 *Node) int32 {
  446. dx := int32(math.Abs(float64(node1.x - node2.x)))
  447. dy := int32(math.Abs(float64(node1.y - node2.y)))
  448. return dx + dy
  449. }