golang数据导向的2D网格库插件tile实现高效路径查找与地图导入导出
Golang 数据导向的 2D 网格库 Tile 实现高效路径查找与地图导入导出
Tile: 数据导向的 2D 网格引擎
这个仓库包含了一个 2D 瓦片地图引擎,采用数据和缓存友好的方式构建。主要目标是提供一个简单、高性能的库来处理游戏中的大规模瓦片地图。
特性
- 紧凑:每个瓦片值长 4 字节,每个网格页长 64 字节,意味着 3000x3000 的网格大约占用 64MB 内存。
- 线程安全:网格是线程安全的,可以通过提供的更新函数进行更新。允许多个 goroutine 并发读写网格而不会产生争用。
- 视图和观察者:当网格上的瓦片更新时,瓦片的观察者会收到更新通知并可以响应变化。
- 零分配(或接近零分配):网格是完全预分配的,这个库提供了几种遍历方式。
- 路径查找:库提供了 A* 路径查找算法来计算两点之间的路径,以及基于 BFS 的位置扫描。
网格和瓦片
库的主要入口是 Grid
,代表一个二维网格,是 Tile
结构的容器。Tile
本质上是指向特定 x,y 坐标的光标,包含以下内容:
- 瓦片的
uint32
值,可用于计算导航或快速检索精灵索引。 T comparable
的状态集,可用于添加额外信息,如瓦片上存在的对象。
创建网格
要创建一个新的 Grid[T]
,首先需要调用 NewGridOf[T]()
方法,它会预分配所需空间并初始化瓦片网格。例如,可以创建一个 1000x1000 的网格:
grid := tile.NewGridOf[string](1000, 1000)
遍历网格
Each()
方法允许你遍历网格中的所有瓦片:
grid.Each(func(p Point, t tile.Tile[string]) {
// ...
})
Within()
方法允许你遍历边界框内的瓦片集:
grid.Within(At(1, 1), At(5, 5), func(p Point, t tile.Tile[string]) {
// ...
})
访问和修改瓦片
At()
方法允许你检索特定 x,y 坐标的瓦片:
if tile, ok := grid.At(50, 100); ok {
// ...
}
WriteAt()
方法允许你更新特定 x,y 坐标的瓦片:
grid.WriteAt(50, 100, tile.Value(0xFF))
MergeAt()
方法允许你原子性地更新瓦片的值:
grid.MergeAt(50, 100, func(v Value) Value {
v += 1
return v
})
MaskAt()
方法允许你原子性地更新瓦片的某些位:
// 假设瓦片的 byte[0] 是 0b01010001
grid.MaskAt(50, 100,
0b00101110, // 只有最后 2 位有效
0b00000011 // 掩码指定我们要更新最后 2 位
)
// 如果原始值是 0b01010001
// 结果将是 0b01010010
路径查找
库提供了几种网格搜索/路径查找函数。它们需要成本函数 func(Tile) uint16
,返回遍历特定瓦片的"成本"。如果成本函数返回 0
,则该瓦片被视为不可通过的障碍物。
Path()
方法用于查找两点之间的路径:
from := At(1, 1)
goal := At(7, 7)
path, distance, found := m.Path(from, goal, func(v tile.Value) uint16{
if isImpassable(v) {
return 0
}
return 1
})
Around()
方法允许你在点周围进行广度优先搜索:
point := At(50, 50)
radius := 5
m.Around(point, radius, func(v tile.Value) uint16{
if isImpassable(v) {
return 0
}
return 1
}, func(p tile.Point, t tile.Tile[string]) {
// ... 找到的瓦片
})
观察者
NewView()
方法创建一个 Observer
,可用于观察边界框内的变化:
view := tile.NewView[string, string](grid, "My View #1")
view.Resize(tile.NewRect(0, 0, 20, 20), func(p tile.Point, t tile.Tile){
// 可选的,现在视图中所有的瓦片
})
// 轮询收件箱(实际上这需要一个 select 和 goroutine)
for {
update := <-view.Inbox
// 对 update.Point, update.Tile 做一些操作
}
MoveBy()
方法允许你在特定方向上移动视图:
view.MoveBy(0, 5, func(p tile.Point, tile tile.Tile){
// 进入我们视图的每个瓦片
})
MoveAt()
方法允许你将视图移动到特定位置:
view.MoveAt(At(10, 10), func(p tile.Point, t tile.Tile){
// 进入我们视图的每个瓦片
})
Resize()
方法允许你调整和更新视图:
viewRect := tile.NewRect(10, 10, 30, 30)
view.Resize(viewRect, func(p tile.Point, t tile.Tile){
// 进入我们视图的每个瓦片
})
Close()
方法应在完成视图时调用:
// 取消订阅通知并关闭视图
view.Close()
保存和加载
库提供了将 Grid
保存到 io.Writer
和从 io.Reader
加载的方法:
// 准备输出缓冲区和压缩器
output := new(bytes.Buffer)
writer, err := flate.NewWriter(output, flate.BestSpeed)
if err != nil {
// ...
}
defer writer.Close() // 确保刷新压缩器
_, err := grid.WriteTo(writer) // 写入网格
if err != nil {
// ...
}
// 准备一个压缩读取器
reader := flate.NewReader(output)
// 读取网格
grid, err := ReadFrom(reader)
if err != nil{
// ...
}
性能基准
库包含了许多微基准测试以确保一切保持快速:
cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkGrid/each-8 868 1358434 ns/op 0 B/op 0 allocs/op
BenchmarkGrid/neighbors-8 66551679 17.87 ns/op 0 B/op 0 allocs/op
BenchmarkGrid/within-8 27207 44753 ns/op 0 B/op 0 allocs/op
BenchmarkGrid/at-8 399067512 2.994 ns/op 0 B/op 0 allocs/op
BenchmarkGrid/write-8 130207965 9.294 ns/op 0 B/op 0 allocs/op
BenchmarkGrid/merge-8 124156794 9.663 ns/op 0 B/op 0 allocs/op
BenchmarkGrid/mask-8 100000000 10.67 ns/op 0 B/op 0 allocs/op
BenchmarkState/range-8 12106854 98.91 ns/op 0 B/op 0 allocs/op
BenchmarkState/add-8 48827727 25.43 ns/op 0 B/op 0 allocs/op
BenchmarkState/del-8 52110474 21.59 ns/op 0 B/op 0 allocs/op
BenchmarkPath/9x9-8 264586 4656 ns/op 16460 B/op 3 allocs/op
BenchmarkPath/300x300-8 601 1937662 ns/op 7801502 B/op 4 allocs/op
BenchmarkPath/381x381-8 363 3304134 ns/op 62394356 B/op 5 allocs/op
BenchmarkPath/384x384-8 171 7165777 ns/op 62394400 B/op 5 allocs/op
BenchmarkPath/3069x3069-8 31 36479106 ns/op 124836075 B/op 4 allocs/op
BenchmarkPath/3072x3072-8 30 34889740 ns/op 124837686 B/op 4 allocs/op
BenchmarkPath/6144x6144-8 142 7594013 ns/op 62395376 B/op 3 allocs/op
BenchmarkAround/3r-8 506857 2384 ns/op 385 B/op 1 allocs/op
BenchmarkAround/5r-8 214280 5539 ns/op 922 B/op 2 allocs/op
BenchmarkAround/10r-8 85723 14017 ns/op 3481 B/op 2 allocs/op
BenchmarkPoint/within-8 1000000000 0.2190 ns/op 0 B/op 0 allocs/op
BenchmarkPoint/within-rect-8 1000000000 0.2195 ns/op 0 B/op 0 allocs/op
BenchmarkStore/save-8 14577 82510 ns/op 8 B/op 1 allocs/op
BenchmarkStore/read-8 3199 364771 ns/op 647419 B/op 7 allocs/op
BenchmarkView/write-8 6285351 188.2 ns/op 48 B/op 1 allocs/op
BenchmarkView/move-8 10000 116953 ns/op 0 B/op 0 allocs/op
完整示例
以下是一个完整的示例,展示如何创建网格、进行路径查找和保存/加载地图:
package main
import (
"bytes"
"compress/flate"
"fmt"
"github.com/kelindar/tile"
)
func main() {
// 创建一个 100x100 的网格
grid := tile.NewGridOf[string](100, 100)
// 设置一些障碍物
for x := 20; x < 80; x++ {
grid.WriteAt(x, 50, tile.Value(0)) // 0 表示不可通过
}
// 查找路径
from := tile.At(10, 10)
to := tile.At(90, 90)
path, _, found := grid.Path(from, to, func(v tile.Value) uint16 {
if v == 0 {
return 0 // 不可通过
}
return 1 // 可通过,成本为1
})
if found {
fmt.Println("找到路径:", path)
} else {
fmt.Println("未找到路径")
}
// 保存网格
var buf bytes.Buffer
writer, _ := flate.NewWriter(&buf, flate.BestSpeed)
grid.WriteTo(writer)
writer.Close()
// 加载网格
reader := flate.NewReader(&buf)
loadedGrid, _ := tile.ReadFrom[string](reader)
reader.Close()
fmt.Println("网格大小:", loadedGrid.Size())
}
这个示例展示了如何:
- 创建一个 100x100 的网格
- 设置一些障碍物
- 使用 A* 算法查找路径
- 将网格保存到压缩缓冲区
- 从压缩缓冲区加载网格
Tile 库提供了高效的数据导向 2D 网格实现,特别适合需要高性能路径查找和地图管理的游戏应用。
更多关于golang数据导向的2D网格库插件tile实现高效路径查找与地图导入导出的实战教程也可以访问 https://www.itying.com/category-94-b0.html