| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- // Package astar 实现了六边形网格的A*寻路算法
- // 采用odd-q offset坐标系(奇数列向上偏移),支持并发安全调用
- // 核心优化:预计算坐标、heap优先队列、对象池复用、视觉直线性惩罚
- package astar6
- import (
- "container/heap"
- "f1-game/internal/constant"
- "fmt"
- "math"
- "slices"
- clog "github.com/cherry-game/cherry/logger"
- )
- // AStar A*寻路算法结构(无状态,可安全被多协程共享)
- type AStar struct {
- config Config
- cache [][]*Node // 节点缓存,二维数组直接索引(只读,初始化后不变)
- }
- // New 创建A*寻路实例
- // 初始化时会构建所有可行走节点及其邻居关系
- // 返回的实例是并发安全的,可被多个goroutine同时调用FindPath
- func New(c Config) *AStar {
- instance := &AStar{config: c}
- instance.init()
- return instance
- }
- // init 初始化寻路器
- // 1. 遍历地图数据创建所有可行走节点
- // 2. 建立节点间的邻居关系
- // 3. 设置默认的关键点提取函数
- func (p *AStar) init() {
- // 初始化二维数组缓存
- rows := p.config.Max.X - p.config.Min.X + 1
- cols := p.config.Max.Y - p.config.Min.Y + 1
- p.cache = make([][]*Node, rows)
- for i := range p.cache {
- p.cache[i] = make([]*Node, cols)
- }
- // 遍历地图范围,创建可行走节点
- for x := p.config.Min.X; x <= p.config.Max.X; x++ {
- for y := p.config.Min.Y; y <= p.config.Max.Y; y++ {
- value := p.config.MapData[x-p.config.Min.X][y-p.config.Min.Y]
- if constant.IsWalkableTile(value) {
- p.addNode(NewNode(x, y, true))
- }
- }
- }
- // 建立邻居关系(六边形拓扑)
- p.initNodeNeighbors()
- }
- // 测试用,设置所有节点为可行走
- func (p *AStar) ResetCache() {
- for _, row := range p.cache {
- for _, node := range row {
- if node != nil {
- node.isWalkable = true
- }
- }
- }
- }
- func (p *AStar) SetNode(x, y int32, isWalkable bool) {
- node := p.GetNode(x, y)
- if node != nil {
- node.SetWalkable(isWalkable)
- }
- }
- // initNodeNeighbors 初始化所有节点的邻居关系
- // 六边形网格采用odd-q坐标系(奇数列上移)
- //
- // 偶数列(x=0,2,4...)的6个邻居:
- //
- // (x-1,y) (x+1,y)
- // (x,y-1) [x,y] (x,y+1)
- // (x-1,y+1) (x+1,y+1)
- //
- // 奇数列(x=1,3,5...)的6个邻居:
- //
- // (x-1,y-1) (x+1,y-1)
- // (x,y-1) [x,y] (x,y+1)
- // (x-1,y) (x+1,y)
- func (p *AStar) initNodeNeighbors() {
- for x := p.config.Min.X; x <= p.config.Max.X; x++ {
- for y := p.config.Min.Y; y <= p.config.Max.Y; y++ {
- node := p.GetNode(x, y)
- if node == nil || !node.isWalkable {
- continue
- }
- if node.IsEven() { // 偶数列
- node.addNeighbor(p.GetNode(x+1, y), ODD_DIAGONAL_DISTINCT) // 右上
- node.addNeighbor(p.GetNode(x+1, y+1), ODD_DIAGONAL_DISTINCT) // 右下
- node.addNeighbor(p.GetNode(x-1, y), ODD_DIAGONAL_DISTINCT) // 左上
- node.addNeighbor(p.GetNode(x-1, y+1), ODD_DIAGONAL_DISTINCT) // 左下
- node.addNeighbor(p.GetNode(x, y-1), ODD_STRAIGHT_DISTINCT) // 上
- node.addNeighbor(p.GetNode(x, y+1), ODD_STRAIGHT_DISTINCT) // 下
- } else { // 奇数列
- node.addNeighbor(p.GetNode(x+1, y), EVEN_DIAGONAL_DISTINCT) // 右下
- node.addNeighbor(p.GetNode(x+1, y-1), EVEN_DIAGONAL_DISTINCT) // 右上
- node.addNeighbor(p.GetNode(x-1, y), EVEN_DIAGONAL_DISTINCT) // 左下
- node.addNeighbor(p.GetNode(x-1, y-1), EVEN_DIAGONAL_DISTINCT) // 左上
- node.addNeighbor(p.GetNode(x, y-1), EVEN_STRAIGHT_DISTINCT) // 上
- node.addNeighbor(p.GetNode(x, y+1), EVEN_STRAIGHT_DISTINCT) // 下
- }
- }
- }
- }
- // FindPath 寻路入口函数(并发安全)
- // 从(x1,y1)到(x2,y2)寻找最优路径
- // 返回路径节点列表和是否找到路径
- // 使用对象池复用搜索上下文,支持多goroutine并发调用
- func (p *AStar) FindPath(ctx *FindContext) ([]*Node, bool) {
- // 回收 Context 对象
- defer releaseContext(ctx)
- start := p.GetNode(ctx.sx, ctx.sy)
- end := p.GetNode(ctx.ex, ctx.ey)
- // 验证起点和终点是否可行走
- if !p.isWalkable(start, ctx) || !p.isWalkable(end, ctx) {
- clog.Debugf("path find failed: start or end is not walkable: (%v -> %v)", start, end)
- return nil, false
- }
- // 初始化搜索状态(用于视觉惩罚计算)
- ctx.start = start
- ctx.end = end
- // 初始化起点:g=0,f=h
- ctx.gMap[start] = 0
- h := p.calH(start, ctx.end)
- item := &heapItem{
- node: start,
- f: h,
- }
- heap.Push(&ctx.openHeap, item)
- ctx.openSet[start] = item
- // 执行A*搜索
- result := p.search(ctx)
- if len(result) == 0 {
- clog.Debugf("path find failed: find empty path: (%v -> %v)", start, end)
- return nil, false
- }
- return result, true
- }
- // search 寻路核心算法
- // 使用A*算法搜索从起点到终点的最优路径
- // 算法流程:
- // 1. 从openHeap取出f值最小的节点作为当前节点
- // 2. 如果当前节点是终点,构建并返回路径
- // 3. 将当前节点加入closeSet
- // 4. 遍历所有邻居,更新路径代价
- // 5. 重复直到找到终点或openHeap为空
- func (p *AStar) search(ctx *FindContext) []*Node {
- for ctx.openHeap.Len() > 0 {
- // 从优先队列取出f值最小的节点
- item := heap.Pop(&ctx.openHeap).(*heapItem)
- current := item.node
- delete(ctx.openSet, current)
- ctx.closeSet[current] = struct{}{}
- // 到达终点,构建路径返回
- if current == ctx.end {
- return p.buildPath(ctx, current)
- }
- // 遍历所有邻居节点(优化:使用静态数组)
- neighborCount := current.neighborCount
- for i := int32(0); i < neighborCount; i++ {
- neighborNode := current.neighborNodes[i]
- cost := current.neighborCosts[i]
- // 跳过已在closeSet中的节点
- if _, closed := ctx.closeSet[neighborNode]; closed {
- continue
- }
- // 跳过不可行走的节点(可能在寻路过程中被动态设置为不可行走)
- if !p.isWalkable(neighborNode, ctx) {
- continue
- }
- // 检查并更新邻居节点的路径
- p.checkPath(ctx, neighborNode, current, cost)
- }
- }
- return nil
- }
- // checkPath 检查并更新节点路径
- // 核心逻辑:
- // - 如果节点已在openSet中,检查新路径是否更优
- // - 如果节点不在openSet中,添加到openSet
- // 参数:
- // - node: 待检查的邻居节点
- // - parent: 当前节点(作为node的父节点)
- // - cost: 从parent到node的移动代价
- // 注:终点信息来自 ctx.end
- // 优化:使用heapItem对象池减少内存分配
- func (p *AStar) checkPath(ctx *FindContext, node, parent *Node, cost int32) {
- // 计算经过parent到达node的g值
- newG := p.calG(ctx, node, parent, cost)
- if item, inOpen := ctx.openSet[node]; inOpen {
- // 节点已在openSet中,检查新路径是否更优
- if newG < ctx.gMap[node] {
- ctx.parentMap[node] = parent
- ctx.gMap[node] = newG
- // 使用索引堆O(1)定位,O(log n)更新
- newF := newG + p.calH(node, ctx.end)
- item.f = newF
- heap.Fix(&ctx.openHeap, item.index) // O(log n)复杂度调整堆
- }
- } else {
- // 新节点,添加到openSet
- // 优化:从对象池获取heapItem
- item := getHeapItem()
- item.node = node
- item.f = newG + p.calH(node, ctx.end)
- ctx.parentMap[node] = parent
- ctx.gMap[node] = newG
- heap.Push(&ctx.openHeap, item)
- ctx.openSet[node] = item
- }
- }
- // buildPath 从终点回溯构建完整路径
- // 通过parentMap逆向遍历,直接构建正向路径,避免使用slices.Reverse
- func (p *AStar) buildPath(ctx *FindContext, node *Node) []*Node {
- // 先计算路径长度
- count := 0
- for n := node; n != nil; n = ctx.parentMap[n] {
- count++
- }
- // 预分配容量,从后往前填充
- result := make([]*Node, count)
- idx := count - 1
- for n := node; n != nil; n = ctx.parentMap[n] {
- result[idx] = n
- idx--
- }
- return result
- }
- // isWalkable 判断节点是否可行走
- // 优先使用配置的自定义判断函数,否则使用节点自身的isWalkable属性
- // 内联到调用处以提升性能
- func (p *AStar) isWalkable(node *Node, ctx *FindContext) bool {
- if node == nil || !node.isWalkable {
- return false
- }
- if p.config.IsWalkableFunc != nil {
- return p.config.IsWalkableFunc(node, ctx)
- }
- return true
- }
- // ContainsNode 检查坐标是否存在对应的节点
- func (p *AStar) ContainsNode(x, y int32) bool {
- return p.GetNode(x, y) != nil
- }
- // GetNode 根据坐标获取节点
- // 优化:使用预计算的常量减少运行时边界检查开销
- func (p *AStar) GetNode(x, y int32) *Node {
- // 预计算边界检查
- maxX := int32(len(p.cache))
- if maxX == 0 {
- return nil
- }
- maxY := int32(len(p.cache[0]))
- // 计算索引
- idx := x - p.config.Min.X
- idy := y - p.config.Min.Y
- // 优化:合并边界检查
- if idx < 0 || idx >= maxX || idy < 0 || idy >= maxY {
- return nil
- }
- return p.cache[idx][idy]
- }
- // addNode 添加节点到缓存
- func (p *AStar) addNode(node *Node) {
- idx := node.x - p.config.Min.X
- idy := node.y - p.config.Min.Y
- p.cache[idx][idy] = node
- }
- // calH 计算启发式距离(曼哈顿距离)
- // 使用预计算的cube坐标,避免运行时转换开销
- // cube坐标下的距离公式: (|x1-x2| + |y1-y2| + |z1-z2|) / 2
- //
- // 启发式强化策略:
- // - 短距离(0-50步):使用基础曼哈顿距离
- // - 中等距离(50-300步):乘以1.2加强引导(常见游戏寻路距离)
- // - 长距离(>300步):乘以1.1避免过度估计(保持A*最优性)
- // 优化:使用位运算和查找表替代乘除法,消除条件分支
- func (p *AStar) calH(node1, node2 *Node) int32 {
- // 优化:使用位运算计算绝对值(无分支)
- dx := node1.cube[0] - node2.cube[0]
- dx = (dx + (dx >> 31)) ^ (dx >> 31)
- dy := node1.cube[1] - node2.cube[1]
- dy = (dy + (dy >> 31)) ^ (dy >> 31)
- dz := node1.cube[2] - node2.cube[2]
- dz = (dz + (dz >> 31)) ^ (dz >> 31)
- // 优化:使用位移替代除法
- base := (dx + dy + dz) >> 1
- // 优化:使用查找表和位运算替代条件分支
- // 预计算启发式乘数:0=短距离×1.0, 1=中等距离×1.2, 2=长距离×1.1
- var multiplier int32 = 10 // 基础乘数
- if base > 50 {
- multiplier = 12 // 中等距离
- if base > 300 {
- multiplier = 11 // 长距离
- }
- }
- return (base * multiplier) / 10
- }
- // calG 计算从起点到当前节点的实际代价
- // g = 父节点g值 + 移动代价 + 转向惩罚 + 视觉直线性惩罚
- // 其中转向惩罚用于避免锯齿路径,视觉直线性惩罚用于优化路径外观
- // 优化:简化转向惩罚计算逻辑,减少分支预测失败
- func (p *AStar) calG(ctx *FindContext, node, parent *Node, cost int32) int32 {
- if parent == nil {
- return cost
- }
- // 获取祖父节点,用于判断方向变化
- grandpa := ctx.parentMap[parent]
- var pdx, pdy int32 // 父节点的移动方向
- if grandpa != nil {
- pdx = parent.x - grandpa.x
- pdy = parent.y - grandpa.y
- }
- // 当前移动方向
- dx := node.x - parent.x
- dy := node.y - parent.y
- // 优化转向惩罚计算,减少分支预测失败
- var turnPenalty int32 = 0
- if pdx != 0 || pdy != 0 { // 不是第一步
- // 使用异或运算快速检测方向变化
- directionChanged := ((dx ^ pdx) | (dy ^ pdy)) != 0
- if directionChanged {
- // 简化的转向判断逻辑
- if dx == 0 || pdx == 0 {
- turnPenalty = COST_TURN
- } else if node.isOdd {
- if dy != pdy && dy != pdy+1 {
- turnPenalty = COST_TURN
- }
- } else {
- if dy != pdy && dy != pdy-1 {
- turnPenalty = COST_TURN
- }
- }
- }
- }
- linearityPenalty := p.calLinearityPenalty(ctx, node, parent, grandpa)
- return ctx.gMap[parent] + cost + turnPenalty + linearityPenalty
- }
- // calLinearityPenalty 计算视觉直线性惩罚(使用预计算坐标)
- // 目的:使路径在视觉上更加平滑,避免不必要的Y方向反向抖动
- // 惩罚策略:
- // 1. 惩罚y值低于起点/终点最小y的节点(防止路径向上偏移过多)
- // 2. 惩罚视觉Y方向反向(在像素坐标下判断,符合实际显示效果)
- // 3. 奖励同方向移动(鼓励保持方向一致性)
- // 优化:完全使用整数运算,消除所有浮点运算
- func (p *AStar) calLinearityPenalty(ctx *FindContext, node, parent, grandpa *Node) int32 {
- if parent == nil || ctx.start == nil || ctx.end == nil {
- return 0
- }
- var penalty int32 = 0
- // 惩罚1:如果y值低于起点和终点的最小y值
- // 防止路径向屏幕上方偏移过多
- // 优化:使用位移替代乘法
- if node.y < ctx.minY {
- penalty += COST_VISUAL_TURN << 1 // 乘以2
- }
- // 检查视觉Y方向反向(需要祖父节点来判断方向变化)
- if grandpa != nil {
- // 优化:使用预计算的整数坐标(×1000),完全消除浮点运算
- prevDyInt := parent.pixelYInt - grandpa.pixelYInt
- currDyInt := node.pixelYInt - parent.pixelYInt
- // Y方向反向(乘积为负表示方向相反)
- // 优化:使用异或运算检测符号变化,比乘法更快
- if (prevDyInt ^ currDyInt) < 0 {
- // 计算当前位置在整条路径中的进度比例(使用整数运算)
- totalDistInt := ctx.end.pixelXInt - ctx.start.pixelXInt
- nodeDistInt := node.pixelXInt - ctx.start.pixelXInt
- // 使用整数运算计算进度和距离中间的距离
- // progress * 1024 表示,512表示0.5(中间位置)
- // 优化:使用位移替代乘除法
- progressInt := int32(512) // 默认0.5
- if totalDistInt > 0 {
- // progress = nodeDist / totalDist,乘以1024
- progressInt = int32((nodeDistInt << 10) / totalDistInt)
- }
- // 离中间位置的距离(512表示中间)
- distFromMiddleInt := progressInt - 512
- if distFromMiddleInt < 0 {
- distFromMiddleInt = -distFromMiddleInt
- }
- // 惩罚公式:基础惩罚(30%) + 距离因子(140%)
- // Cost_Visual_Turn * (0.3 + distFromMiddle*1.4)
- // 优化:使用预计算和位运算
- // basePenalty = Cost_Visual_Turn * 3 / 10
- // distPenalty = Cost_Visual_Turn * distFromMiddleInt * 14 / 100
- basePenalty := int32((COST_VISUAL_TURN * 3) / 10)
- distPenalty := int32((COST_VISUAL_TURN * distFromMiddleInt * 14) / 100)
- penalty += basePenalty + distPenalty
- }
- // 同方向移动给予小奖励,鼓励保持方向一致性
- // 优化:使用位与运算检测同号
- if (prevDyInt & currDyInt) >= 0 {
- penalty -= 1
- }
- }
- return penalty
- }
- // Print 可视化打印路径
- // 用于调试,将路径在地图上以ASCII图形展示
- // 图例:◉ 起点, ◦ 路径点, ● 终点
- func (p *AStar) Print(list []*Node) {
- if len(list) < 1 {
- return
- }
- fromNode := list[0]
- toNode := list[len(list)-1]
- mapText := "\n"
- mapText += fmt.Sprintf(" Rectangle: [(%d,%d) -> (%d,%d)] \n", fromNode.x, fromNode.y, toNode.x, toNode.y)
- mapText += fmt.Sprintf(" Pathing: %v\n", list)
- mapText += " Pattern: start = ◉, point = ◦, end = ●\n"
- mapText += "Coordinate: row = x, col = y\n\n"
- for x := range p.config.Min.X {
- // 偶数行缩进,模拟六边形交错布局
- if x%2 == 0 {
- mapText += " "
- }
- for y := range p.config.Max.Y {
- if slices.ContainsFunc(list, func(node *Node) bool { return node.x == x && node.y == y }) {
- if x == fromNode.x && y == fromNode.y {
- mapText += "◉ " // 起点
- } else if x == toNode.x && y == toNode.y {
- mapText += "● " // 终点
- } else {
- mapText += "◦ " // 路径点
- }
- } else {
- mapText += fmt.Sprintf("%d ", p.config.MapData[x][y])
- }
- }
- mapText += "\n"
- }
- clog.Debug(mapText)
- }
- func (p *AStar) GetDistance(sx int32, sy int32, ex int32, ey int32) int32 {
- var (
- start = p.GetNode(sx, sy)
- end = p.GetNode(ex, ey)
- )
- if start == nil || end == nil {
- return math.MaxInt32
- }
- return p.calDistance(start, end)
- }
- func (p *AStar) calDistance(node1, node2 *Node) int32 {
- dx := int32(math.Abs(float64(node1.x - node2.x)))
- dy := int32(math.Abs(float64(node1.y - node2.y)))
- return dx + dy
- }
|