diff --git a/utils/g2d/path/heap.go b/utils/g2d/path/heap.go new file mode 100644 index 0000000..dcfe3f8 --- /dev/null +++ b/utils/g2d/path/heap.go @@ -0,0 +1,27 @@ +package path + +type h []*Node + +func (slf *h) Len() int { + return len(*slf) +} + +func (slf *h) Less(i, j int) bool { + return (*slf)[i].cost < (*slf)[j].cost +} + +func (slf *h) Swap(i, j int) { + (*slf)[i], (*slf)[j] = (*slf)[j], (*slf)[i] +} + +func (slf *h) Push(x any) { + *slf = append(*slf, x.(*Node)) +} + +func (slf *h) Pop() any { + old := *slf + n := len(old) + x := old[n-1] + *slf = old[0 : n-1] + return x +} diff --git a/utils/g2d/path/landform.go b/utils/g2d/path/landform.go new file mode 100644 index 0000000..8578fb9 --- /dev/null +++ b/utils/g2d/path/landform.go @@ -0,0 +1,89 @@ +package path + +import ( + "github.com/kercylan98/minotaur/utils/g2d" +) + +func NewLandform(pos int, features ...*LandformFeature) *Landform { + if len(features) == 0 { + panic("path landforms without any features") + } + pl := &Landform{ + pos: pos, + features: features, + } + + return pl +} + +// Landform 路径地貌表示路径地图上的一个点 +// - 末尾特征将决定该路径地貌是否可行走 +type Landform struct { + width int // 所在路径覆盖宽度 + pos int // 位置 + features []*LandformFeature // 地貌特征 + original []*LandformFeature // 原始地貌 + totalCost float64 // 总消耗 +} + +// GetCoordinate 获取这个路径地貌指向的 x 和 y 坐标 +// - 建议通过 GetPos 来进行获取,这样可以避免一次转换 +func (slf *Landform) GetCoordinate() (x, y int) { + return g2d.PosToCoordinate(slf.width, slf.pos) +} + +// GetPos 获取这个路径地貌指向的 pos 位置 +func (slf *Landform) GetPos() int { + return slf.pos +} + +// GetTotalCost 获取这个路径地貌的总特征消耗 +func (slf *Landform) GetTotalCost() float64 { + return slf.totalCost +} + +// Walkable 指示了该路径地貌是否可以在上面行走或者应该避免在上面行走 +func (slf *Landform) Walkable() bool { + return slf.features[len(slf.features)-1].Walkable() +} + +// GetFeatures 获取这个路径地貌的特征 +func (slf *Landform) GetFeatures() []*LandformFeature { + return slf.features +} + +// SetFeatures 设置这个路径地貌的特征 +func (slf *Landform) SetFeatures(features ...*LandformFeature) { + slf.features = features + slf.original = nil + slf.totalCost = 0 + for _, feature := range slf.features { + slf.totalCost += feature.GetCost() + } +} + +// SetFeaturesWithRecoverable 通过可恢复的方式设置这个路径地貌的特征 +// - 使用该函数设置地貌特征后续也应该继续使用该函数进行地貌特征修改,如果中途使用 SetFeatures 修改地貌特征后将导致 Recover 不可用 +func (slf *Landform) SetFeaturesWithRecoverable(features ...*LandformFeature) { + if slf.original == nil { + slf.original = slf.features + } + slf.features = features + slf.totalCost = 0 + for _, feature := range slf.features { + slf.totalCost += feature.GetCost() + } +} + +// Recover 恢复这个路径地貌的特征 +func (slf *Landform) Recover() { + if slf.original == nil { + return + } + slf.features = slf.original + slf.original = nil + slf.totalCost = 0 + for _, feature := range slf.features { + slf.totalCost += feature.GetCost() + } +} diff --git a/utils/g2d/path/landform_feature.go b/utils/g2d/path/landform_feature.go new file mode 100644 index 0000000..d5d6b81 --- /dev/null +++ b/utils/g2d/path/landform_feature.go @@ -0,0 +1,28 @@ +package path + +var ( + LandformFeatureRoad = NewLandformFeature(1, true) // 道路 + LandformFeatureWater = NewLandformFeature(1.75, true) // 水 +) + +// NewLandformFeature 返回一个新的路径地貌特征, cost 将表示在该地貌上行走的消耗,walkable 则表示了该地貌是否支持行走 +// - 在基于 Terrain 使用时, LandformFeature 必须被声明为全局变量后再进行使用,例如 LandformFeatureRoad +func NewLandformFeature(cost float64, walkable bool) *LandformFeature { + return &LandformFeature{cost: cost, walkable: walkable} +} + +// LandformFeature 表示了路径地貌的特征 +type LandformFeature struct { + cost float64 // 移动消耗 + walkable bool // 适宜步行 +} + +// GetCost 获取在该路径地貌行走的消耗,这影响了该路径地貌是否是理想的选择 +func (slf *LandformFeature) GetCost() float64 { + return slf.cost +} + +// Walkable 指示了该路径地貌是否可以在上面行走或者应该避免在上面行走 +func (slf *LandformFeature) Walkable() bool { + return slf.walkable +} diff --git a/utils/g2d/path/node.go b/utils/g2d/path/node.go new file mode 100644 index 0000000..cc68f0e --- /dev/null +++ b/utils/g2d/path/node.go @@ -0,0 +1,7 @@ +package path + +type Node struct { + landform *Landform + parent *Node + cost float64 +} diff --git a/utils/g2d/path/path.go b/utils/g2d/path/path.go new file mode 100644 index 0000000..3773c56 --- /dev/null +++ b/utils/g2d/path/path.go @@ -0,0 +1,14 @@ +package path + +type Path struct { + points []*Landform + currentIndex int +} + +func (slf *Path) GetPoints() []*Landform { + return slf.points +} + +func (slf *Path) GetCurrentIndex() int { + return slf.currentIndex +} diff --git a/utils/g2d/path/terrain.go b/utils/g2d/path/terrain.go new file mode 100644 index 0000000..4ea76e0 --- /dev/null +++ b/utils/g2d/path/terrain.go @@ -0,0 +1,196 @@ +package path + +import ( + "container/heap" + "github.com/kercylan98/minotaur/utils/g2d" +) + +// NewTerrain 返回一个大小为 width 和 height 的新的路径覆盖信息,landformWidth 和 landformHeight 将对每 +// 个路径地貌的尺寸进行描述,用于世界位置的转化,例如当 landformWidth 和 landformHeight 为 2, 2 时,路径 +// 地貌位于 2, 3 的情况下,它的世界位置将是 4, 6 +// - 地貌特征将默认统一为 PathLandformFeatureRoad +func NewTerrain(width, height, landformWidth, landformHeight int) *Terrain { + path := &Terrain{ + features: map[*LandformFeature]map[int]struct{}{}, + width: width, + height: height, + landformWidth: landformWidth, + landformHeight: landformHeight, + } + path.matrix = make([]*Landform, width*height) + for pos := 0; pos < len(path.matrix); pos++ { + path.matrix[pos] = NewLandform(pos, LandformFeatureRoad) + } + path.refreshFeatures() + return path +} + +// NewPathWithMatrix 基于特定的 matrix 返回一个新的路径覆盖信息 +// - 可参照于 NewTerrain +func NewPathWithMatrix(matrix []*Landform, width, height, landformWidth, landformHeight int) *Terrain { + path := &Terrain{ + matrix: matrix, + width: width, + height: height, + landformWidth: landformWidth, + landformHeight: landformHeight, + } + path.refreshFeatures() + return path +} + +type Terrain struct { + matrix []*Landform // 矩阵 + width, height int // 矩阵宽高 + landformWidth, landformHeight int // 地貌宽高 + features map[*LandformFeature]map[int]struct{} // 标注了特定特征的路径地貌位置 +} + +// Get 返回 x 和 y 指向的地貌信息 +// - 通常更建议使用 GetWithPos 进行获取,因为这样可以减少一次转换 +func (slf *Terrain) Get(x, y int) *Landform { + return slf.matrix[g2d.CoordinateToPos(slf.width, x, y)] +} + +// GetWithPos 返回 pos 指向的地貌信息 +func (slf *Terrain) GetWithPos(pos int) *Landform { + return slf.matrix[pos] +} + +// GetHeight 返回这个路径覆盖的范围高度 +func (slf *Terrain) GetHeight() int { + return slf.height +} + +// GetWidth 返回这个路径覆盖的范围宽度 +func (slf *Terrain) GetWidth() int { + return slf.width +} + +// GetAll 获取所有地貌信息 +func (slf *Terrain) GetAll() []*Landform { + return slf.matrix +} + +// GetPath 返回一个从起点位置到目标位置的路径 +// - 可以通过参数 diagonals 控制是否支持沿对角线进行移动 +// - 可以通过参数 wallsBlockDiagonals 控制在进行对角线移动时是否允许穿越不可通行的区域 +func (slf *Terrain) GetPath(startPos, endPos int, diagonals, wallsBlockDiagonals bool) *Path { + start, end := slf.GetWithPos(startPos), slf.GetWithPos(endPos) + + var nodes h + var checkedLandforms = make(map[int]struct{}) + var path = new(Path) + heap.Push(&nodes, &Node{landform: end, cost: end.GetTotalCost()}) + + if !start.Walkable() || !end.Walkable() { + return nil + } + + for { + if len(nodes) == 0 { + break + } + + node := heap.Pop(&nodes).(*Node) + if node.landform == start { + var t = node + for true { + path.points = append(path.points, t.landform) + t = t.parent + if t == nil { + break + } + } + break + } + + for _, adjacent := range g2d.GetAdjacentTranslatePos(slf.matrix, slf.width, node.landform.pos) { + landform := slf.GetWithPos(adjacent) + n := &Node{landform: landform, parent: node, cost: landform.GetTotalCost() + node.cost} + if _, exist := checkedLandforms[adjacent]; n.landform.Walkable() && !exist { + heap.Push(&nodes, n) + checkedLandforms[adjacent] = struct{}{} + } + } + + if diagonals { + var up, down, left, right bool + if upPos := node.landform.pos - slf.width; upPos >= 0 { + up = slf.GetWithPos(upPos).Walkable() + } + if downPos := node.landform.pos + slf.width; downPos < len(slf.matrix) { + down = slf.GetWithPos(downPos).Walkable() + } + row := node.landform.pos / slf.width + if leftPos := node.landform.pos - 1; row == (leftPos / slf.width) { + left = slf.GetWithPos(leftPos).Walkable() + } + if rightPos := node.landform.pos + 1; row == (rightPos / slf.width) { + right = slf.GetWithPos(rightPos).Walkable() + } + + diagonalCost := .414 + + size := len(slf.matrix) + currentRow := node.landform.pos / slf.width + if topLeft := node.landform.pos - slf.width - 1; topLeft >= 0 && currentRow-1 == (topLeft/slf.width) { + landform := slf.GetWithPos(topLeft) + n := &Node{landform: landform, parent: node, cost: landform.GetTotalCost() + node.cost + diagonalCost} + if _, exist := checkedLandforms[topLeft]; n.landform.Walkable() && !exist && (!wallsBlockDiagonals || (left && up)) { + heap.Push(&nodes, n) + checkedLandforms[topLeft] = struct{}{} + } + } + if topRight := node.landform.pos - slf.width + 1; topRight >= 0 && currentRow-1 == (topRight/slf.width) { + landform := slf.GetWithPos(topRight) + n := &Node{landform: landform, parent: node, cost: landform.GetTotalCost() + node.cost + diagonalCost} + if _, exist := checkedLandforms[topRight]; n.landform.Walkable() && !exist && (!wallsBlockDiagonals || (right && up)) { + heap.Push(&nodes, n) + checkedLandforms[topRight] = struct{}{} + } + } + if bottomLeft := node.landform.pos + slf.width - 1; bottomLeft < size && currentRow+1 == (bottomLeft/slf.width) { + landform := slf.GetWithPos(bottomLeft) + n := &Node{landform: landform, parent: node, cost: landform.GetTotalCost() + node.cost + diagonalCost} + if _, exist := checkedLandforms[bottomLeft]; n.landform.Walkable() && !exist && (!wallsBlockDiagonals || (left && down)) { + heap.Push(&nodes, n) + checkedLandforms[bottomLeft] = struct{}{} + } + } + if bottomRight := node.landform.pos + slf.width + 1; bottomRight < size && currentRow+1 == (bottomRight/slf.width) { + landform := slf.GetWithPos(bottomRight) + n := &Node{landform: landform, parent: node, cost: landform.GetTotalCost() + node.cost + diagonalCost} + if _, exist := checkedLandforms[bottomRight]; n.landform.Walkable() && !exist && (!wallsBlockDiagonals || (right && down)) { + heap.Push(&nodes, n) + checkedLandforms[bottomRight] = struct{}{} + } + } + + } + } + + return path + +} + +// 刷新地貌特征标注信息 +// - 冗余:已使用过的地貌特征类型在未使用后不会被删除 +func (slf *Terrain) refreshFeatures() { + for _, positions := range slf.features { + for pos := range positions { + delete(positions, pos) + } + } + + for pos, landform := range slf.matrix { + for _, feature := range landform.GetFeatures() { + positions, exist := slf.features[feature] + if !exist { + positions = map[int]struct{}{} + slf.features[feature] = positions + } + positions[pos] = struct{}{} + } + } +}