diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/aoi.iml b/.idea/aoi.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/aoi.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..786eef4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/aoi.go b/aoi.go new file mode 100644 index 0000000..987caa3 --- /dev/null +++ b/aoi.go @@ -0,0 +1,12 @@ +package aoi + +type AOI interface { + Add(entity *Entity) // 添加实体 + Delete(entity *Entity) // 移除实体 + Search(entity *Entity) (result []*Entity) // 范围查询 +} + +type Entity struct { + X, Y float64 + Name string +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e6a9eb9 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/knight0zh/aoi + +go 1.17 + +require github.com/stretchr/testify v1.8.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5164829 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid.go b/grid.go new file mode 100644 index 0000000..faa9e41 --- /dev/null +++ b/grid.go @@ -0,0 +1,112 @@ +package aoi + +import "sync" + +// Grid 格子 +type Grid struct { + GID int //格子ID + Entities sync.Map //当前格子内的实体 +} + +// GridManger AOI九宫格实现矩形 +type GridManger struct { + StartX int // X区域左边界坐标 + StartY int // Y区域上边界坐标 + AreaWidth int // 格子宽度(长=宽) + GridCount int // 格子数量 + grids map[int]*Grid +} + +func NewGrid(gid int) *Grid { + return &Grid{ + GID: gid, + } +} + +func NewGridManger(startX, startY, areaWidth, gridCount int) AOI { + manager := &GridManger{ + StartX: startX, + StartY: startY, + AreaWidth: areaWidth, + GridCount: gridCount, + grids: make(map[int]*Grid), + } + + for y := 0; y < gridCount; y++ { + for x := 0; x < gridCount; x++ { + //格子编号:ID = IDy *nx + IDx (利用格子坐标得到格子编号) + gID := y*gridCount + x + manager.grids[gID] = NewGrid(gID) + } + } + + return manager +} + +func (g *GridManger) gridWidth() int { + return g.AreaWidth / g.GridCount +} + +// GetGIDByPos 通过横纵坐标获取对应的格子ID +func (g *GridManger) GetGIDByPos(entity *Entity) int { + gx := (int(entity.X) - g.StartX) / g.gridWidth() + gy := (int(entity.Y) - g.StartY) / g.gridWidth() + + return gy*g.GridCount + gx +} + +// GetSurroundGrids 根据格子的gID得到当前周边的九宫格信息 +func (g *GridManger) GetSurroundGrids(gID int) (grids []*Grid) { + if _, ok := g.grids[gID]; !ok { + return + } + grids = append(grids, g.grids[gID]) + + // 根据gID, 得到格子所在的坐标 + x, y := gID%g.GridCount, gID/g.GridCount + + // 分别将这8个方向的方向向量按顺序写入x, y的分量数组 + dx := []int{-1, -1, -1, 0, 0, 1, 1, 1} + dy := []int{-1, 0, 1, -1, 1, -1, 0, 1} + + surroundGID := make([]int, 0) + for i := 0; i < 8; i++ { + newX := x + dx[i] + newY := y + dy[i] + + if newX >= 0 && newX < g.GridCount && newY >= 0 && newY < g.GridCount { + surroundGID = append(surroundGID, newY*g.GridCount+newX) + } + } + + for _, gID := range surroundGID { + grids = append(grids, g.grids[gID]) + } + + return +} + +func (g *GridManger) Add(entity *Entity) { + ID := g.GetGIDByPos(entity) + grid := g.grids[ID] + grid.Entities.Store(entity.Name, entity) +} + +func (g *GridManger) Delete(entity *Entity) { + ID := g.GetGIDByPos(entity) + grid := g.grids[ID] + grid.Entities.Delete(entity.Name) +} + +func (g *GridManger) Search(entity *Entity) (result []*Entity) { + ID := g.GetGIDByPos(entity) + grids := g.GetSurroundGrids(ID) + for _, grid := range grids { + grid.Entities.Range(func(_, value interface{}) bool { + result = append(result, value.(*Entity)) + return true + }) + } + + return +} diff --git a/grid_test.go b/grid_test.go new file mode 100644 index 0000000..9f3086f --- /dev/null +++ b/grid_test.go @@ -0,0 +1,166 @@ +package aoi + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "math/rand" + "sort" + "sync" + "testing" + "time" +) + +/** + 0 50 100 150 200 + ----------------------------- +0 | 0 1 2 3 4 +50 | 5 6 7 8 9 +100 | 10 11 12 13 14 +150 | 15 16 17 18 19 +200 | 20 21 22 23 24 + +*/ +func TestGridManger_GetSurroundGrids(t *testing.T) { + aol := NewGridManger(0, 0, 250, 5) + manger := aol.(*GridManger) + tests := []struct { + entity *Entity + want []int + }{ + { + entity: &Entity{ + X: 0, Y: 0, + }, + want: []int{0, 1, 5, 6}, + }, + { + entity: &Entity{ + X: 150, Y: 0, + }, + want: []int{2, 3, 4, 7, 8, 9}, + }, + { + entity: &Entity{ + X: 50, Y: 50, + }, + want: []int{0, 1, 2, 5, 6, 7, 10, 11, 12}, + }, + { + entity: &Entity{ + X: 200, Y: 100, + }, + want: []int{8, 9, 13, 14, 18, 19}, + }, + { + entity: &Entity{ + X: 200, Y: 200, + }, + want: []int{18, 19, 23, 24}, + }, + } + + for _, tt := range tests { + ID := manger.GetGIDByPos(tt.entity) + grids := manger.GetSurroundGrids(ID) + gID := make([]int, 0) + for _, grid := range grids { + gID = append(gID, grid.GID) + } + sort.Ints(gID) + assert.Equal(t, tt.want, gID) + } +} + +func TestNewGridManger(t *testing.T) { + aol := NewGridManger(0, 0, 250, 5) + manger := aol.(*GridManger) + entities := []*Entity{ + { + X: 0, Y: 0, Name: "a", + }, + { + X: 50, Y: 0, Name: "b", + }, + { + X: 100, Y: 0, Name: "c", + }, + { + X: 50, Y: 0, Name: "d", + }, + { + X: 50, Y: 50, Name: "e", + }, + { + X: 50, Y: 100, Name: "f", + }, + { + X: 100, Y: 0, Name: "g", + }, + { + X: 100, Y: 50, Name: "h", + }, + { + X: 100, Y: 100, Name: "i", + }, + } + + for _, entity := range entities { + manger.Add(entity) + } + + search := manger.Search(&Entity{X: 50, Y: 50}) + result := make([]string, 0) + for _, entity := range search { + result = append(result, entity.Name) + } + sort.Strings(result) + assert.Equal(t, []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, result) + + manger.Delete(&Entity{X: 100, Y: 100, Name: "i"}) + search2 := manger.Search(&Entity{X: 50, Y: 50}) + result2 := make([]string, 0) + for _, entity := range search2 { + result2 = append(result2, entity.Name) + } + sort.Strings(result2) + assert.Equal(t, []string{"a", "b", "c", "d", "e", "f", "g", "h"}, result2) +} + +func BenchmarkGridManger(b *testing.B) { + var wg sync.WaitGroup + aol := NewGridManger(0, 0, 250, 5) + manger := aol.(*GridManger) + + rand.Seed(time.Now().UnixNano()) + for i := 0; i < b.N; i++ { + wg.Add(3000) + for j := 0; j < 1000; j++ { + go func() { + manger.Add(&Entity{ + X: float64(rand.Intn(5) * 10), + Y: float64(rand.Intn(5) * 10), + Name: fmt.Sprintf("name%d", rand.Intn(50)), + }) + wg.Done() + }() + + go func() { + manger.Delete(&Entity{ + X: float64(rand.Intn(5) * 10), + Y: float64(rand.Intn(5) * 10), + Name: fmt.Sprintf("name%d", rand.Intn(50)), + }) + wg.Done() + }() + + go func() { + manger.Search(&Entity{ + X: float64(rand.Intn(5) * 10), + Y: float64(rand.Intn(5) * 10), + }) + wg.Done() + }() + } + wg.Wait() + } +} diff --git a/quadtree.go b/quadtree.go new file mode 100644 index 0000000..cb30b8a --- /dev/null +++ b/quadtree.go @@ -0,0 +1,37 @@ +package aoi + +import "sync" + +const ( + leftUp int = iota + rightUp + leftDown + rightDown +) + +type Node struct { + AreaWidth int // 格子宽度(长=宽) + XStart int // 起始范围 + YStart int // 起始范围 + Deep int // 深度 + Leaf bool // 是否为叶子节点 + Parent *Node // 父节点 + Child [4]*Node // 子节点 + Entities sync.Map // 实体 +} + +type QuadTree struct { + Root *Node +} + +func (q QuadTree) Add(entity *Entity) { + panic("implement me") +} + +func (q QuadTree) Delete(entity *Entity) { + panic("implement me") +} + +func (q QuadTree) Search(entity *Entity) (result []*Entity) { + panic("implement me") +} diff --git a/quadtree_test.go b/quadtree_test.go new file mode 100644 index 0000000..5d87797 --- /dev/null +++ b/quadtree_test.go @@ -0,0 +1 @@ +package aoi