golang数据导向的2D网格库插件tile实现高效路径查找与地图导入导出

Golang 数据导向的 2D 网格库 Tile 实现高效路径查找与地图导入导出

Tile: 数据导向的 2D 网格引擎

Tile Logo

这个仓库包含了一个 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())
}

这个示例展示了如何:

  1. 创建一个 100x100 的网格
  2. 设置一些障碍物
  3. 使用 A* 算法查找路径
  4. 将网格保存到压缩缓冲区
  5. 从压缩缓冲区加载网格

Tile 库提供了高效的数据导向 2D 网格实现,特别适合需要高性能路径查找和地图管理的游戏应用。


更多关于golang数据导向的2D网格库插件tile实现高效路径查找与地图导入导出的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang数据导向的2D网格库插件tile实现高效路径查找与地图导入导出的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 2D网格库实现:高效路径查找与地图导入导出

下面我将介绍一个基于数据导向设计的2D网格库实现,包含高效的路径查找算法(A*)以及地图的导入导出功能。

核心数据结构

package tile

import (
	"encoding/json"
	"errors"
	"math"
)

// 定义瓦片类型
type TileType int

const (
	Empty TileType = iota
	Wall
	Water
	Forest
	Road
)

// 网格单元
type Cell struct {
	Type       TileType
	Weight     float64 // 移动权重(用于路径查找)
	X, Y       int     // 坐标
	Passable   bool    // 是否可通行
	Properties map[string]interface{}
}

// 2D网格地图
type Grid struct {
	Width, Height int
	Cells         [][]*Cell
}

// 新建网格
func NewGrid(width, height int) *Grid {
	grid := &Grid{
		Width:  width,
		Height: height,
		Cells:  make([][]*Cell, height),
	}

	for y := 0; y < height; y++ {
		grid.Cells[y] = make([]*Cell, width)
		for x := 0; x < width; x++ {
			grid.Cells[y][x] = &Cell{
				X:          x,
				Y:          y,
				Type:       Empty,
				Weight:     1.0,
				Passable:   true,
				Properties: make(map[string]interface{}),
			}
		}
	}
	return grid
}

A*路径查找实现

// A*算法节点
type aStarNode struct {
	cell      *Cell
	parent    *aStarNode
	gScore    float64 // 从起点到当前节点的成本
	hScore    float64 // 启发式估计到终点的成本
	fScore    float64 // gScore + hScore
	heapIndex int     // 在堆中的索引
}

// 实现优先队列接口
type priorityQueue []*aStarNode

func (pq priorityQueue) Len() int { return len(pq) }

func (pq priorityQueue) Less(i, j int) bool {
	return pq[i].fScore < pq[j].fScore
}

func (pq priorityQueue) Swap(i, j int) {
	pq[i], pq[j] = pq[j], pq[i]
	pq[i].heapIndex = i
	pq[j].heapIndex = j
}

func (pq *priorityQueue) Push(x interface{}) {
	n := len(*pq)
	node := x.(*aStarNode)
	node.heapIndex = n
	*pq = append(*pq, node)
}

func (pq *priorityQueue) Pop() interface{} {
	old := *pq
	n := len(old)
	node := old[n-1]
	node.heapIndex = -1
	*pq = old[0 : n-1]
	return node
}

// A*路径查找
func (g *Grid) FindPath(startX, startY, endX, endY int) ([]*Cell, error) {
	if !g.isValid(startX, startY) || !g.isValid(endX, endY) {
		return nil, errors.New("invalid start or end position")
	}

	startCell := g.Cells[startY][startX]
	endCell := g.Cells[endY][endX]

	if !startCell.Passable || !endCell.Passable {
		return nil, errors.New("start or end cell is not passable")
	}

	openSet := make(priorityQueue, 0)
	heap.Init(&openSet)

	// 初始化起始节点
	startNode := &aStarNode{
		cell:   startCell,
		gScore: 0,
		hScore: heuristic(startCell, endCell),
		fScore: heuristic(startCell, endCell),
	}
	heap.Push(&openSet, startNode)

	// 记录所有访问过的节点
	cameFrom := make(map[*Cell]*aStarNode)
	gScore := make(map[*Cell]float64)
	gScore[startCell] = 0

	for openSet.Len() > 0 {
		current := heap.Pop(&openSet).(*aStarNode)

		if current.cell == endCell {
			return reconstructPath(cameFrom, current), nil
		}

		// 检查所有相邻节点
		for _, neighbor := range g.getNeighbors(current.cell.X, current.cell.Y) {
			if !neighbor.Passable {
				continue
			}

			tentativeGScore := gScore[current.cell] + neighbor.Weight

			if existingGScore, ok := gScore[neighbor]; !ok || tentativeGScore < existingGScore {
				cameFrom[neighbor] = current
				gScore[neighbor] = tentativeGScore

				h := heuristic(neighbor, endCell)
				f := tentativeGScore + h

				// 检查节点是否已经在开放集中
				found := false
				for _, node := range openSet {
					if node.cell == neighbor {
						found = true
						node.gScore = tentativeGScore
						node.hScore = h
						node.fScore = f
						heap.Fix(&openSet, node.heapIndex)
						break
					}
				}

				if !found {
					newNode := &aStarNode{
						cell:   neighbor,
						gScore: tentativeGScore,
						hScore: h,
						fScore: f,
					}
					heap.Push(&openSet, newNode)
				}
			}
		}
	}

	return nil, errors.New("path not found")
}

// 启发式函数(曼哈顿距离)
func heuristic(a, b *Cell) float64 {
	return math.Abs(float64(a.X-b.X)) + math.Abs(float64(a.Y-b.Y))
}

// 重建路径
func reconstructPath(cameFrom map[*Cell]*aStarNode, current *aStarNode) []*Cell {
	path := []*Cell{current.cell}
	for {
		prev, ok := cameFrom[current.cell]
		if !ok {
			break
		}
		path = append([]*Cell{prev.cell}, path...)
		current = prev
	}
	return path
}

// 获取相邻单元格
func (g *Grid) getNeighbors(x, y int) []*Cell {
	neighbors := make([]*Cell, 0, 8)
	directions := [8][2]int{
		{-1, -1}, {0, -1}, {1, -1},
		{-1, 0}, {1, 0},
		{-1, 1}, {0, 1}, {1, 1},
	}

	for _, dir := range directions {
		nx, ny := x+dir[0], y+dir[1]
		if g.isValid(nx, ny) {
			neighbors = append(neighbors, g.Cells[ny][nx])
		}
	}
	return neighbors
}

func (g *Grid) isValid(x, y int) bool {
	return x >= 0 && x < g.Width && y >= 0 && y < g.Height
}

地图导入导出

// 地图JSON结构
type gridJSON struct {
	Width  int            `json:"width"`
	Height int            `json:"height"`
	Cells  [][]cellJSON   `json:"cells"`
}

type cellJSON struct {
	Type       TileType               `json:"type"`
	Weight     float64               `json:"weight"`
	Passable   bool                  `json:"passable"`
	Properties map[string]interface{} `json:"properties"`
}

// 导出为JSON
func (g *Grid) ToJSON() ([]byte, error) {
	gridJSON := gridJSON{
		Width:  g.Width,
		Height: g.Height,
		Cells:  make([][]cellJSON, g.Height),
	}

	for y := 0; y < g.Height; y++ {
		gridJSON.Cells[y] = make([]cellJSON, g.Width)
		for x := 0; x < g.Width; x++ {
			cell := g.Cells[y][x]
			gridJSON.Cells[y][x] = cellJSON{
				Type:       cell.Type,
				Weight:     cell.Weight,
				Passable:   cell.Passable,
				Properties: cell.Properties,
			}
		}
	}

	return json.MarshalIndent(gridJSON, "", "  ")
}

// 从JSON导入
func FromJSON(data []byte) (*Grid, error) {
	var gridJSON gridJSON
	if err := json.Unmarshal(data, &gridJSON); err != nil {
		return nil, err
	}

	grid := NewGrid(gridJSON.Width, gridJSON.Height)

	for y := 0; y < grid.Height; y++ {
		for x := 0; x < grid.Width; x++ {
			cellJSON := gridJSON.Cells[y][x]
			cell := grid.Cells[y][x]
			cell.Type = cellJSON.Type
			cell.Weight = cellJSON.Weight
			cell.Passable = cellJSON.Passable
			cell.Properties = cellJSON.Properties
		}
	}

	return grid, nil
}

使用示例

func main() {
	// 创建10x10网格
	grid := NewGrid(10, 10)
	
	// 设置一些障碍物
	for i := 2; i < 8; i++ {
		grid.Cells[i][5].Type = Wall
		grid.Cells[i][5].Passable = false
	}
	
	// 查找路径
	path, err := grid.FindPath(0, 0, 9, 9)
	if err != nil {
		fmt.Println("Path not found:", err)
		return
	}
	
	fmt.Println("Path found with length:", len(path))
	
	// 导出地图
	jsonData, err := grid.ToJSON()
	if err != nil {
		fmt.Println("Export error:", err)
		return
	}
	
	// 导入地图
	newGrid, err := FromJSON(jsonData)
	if err != nil {
		fmt.Println("Import error:", err)
		return
	}
	
	fmt.Println("Imported grid size:", newGrid.Width, "x", newGrid.Height)
}

性能优化建议

  1. 对象池:对于频繁创建/销毁的A*节点,可以使用对象池减少GC压力
  2. 内存布局:将单元格数据改为连续内存布局(结构体数组而非指针数组)
  3. 并行处理:对于大型地图,可以将地图分块并行处理路径查找
  4. 缓存:缓存常用路径查找结果
  5. 分层寻路:实现分层寻路(Hierarchical Pathfinding)减少计算量

这个实现提供了基本的2D网格功能,包括高效的A*路径查找和地图的序列化/反序列化。您可以根据需要扩展更多功能,如动态障碍物、区域标记等。

回到顶部