寻路功能实现

This commit is contained in:
kercylan98 2023-06-14 12:32:28 +08:00
parent ce77f513a4
commit 2add117daa
6 changed files with 361 additions and 0 deletions

27
utils/g2d/path/heap.go Normal file
View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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
}

7
utils/g2d/path/node.go Normal file
View File

@ -0,0 +1,7 @@
package path
type Node struct {
landform *Landform
parent *Node
cost float64
}

14
utils/g2d/path/path.go Normal file
View File

@ -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
}

196
utils/g2d/path/terrain.go Normal file
View File

@ -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{}{}
}
}
}