c语言编程笔录

首页 >   > 笔记大全

笔记大全

Go语言如何实现AOI区域视野管理

更新时间:2023-11-10

什么是AOI区域视野管理?

AOI(Area of Interest)指的是玩家的视野范围,将玩家当前所处的场景、区域内的其他玩家或敌人等生物实体纳入到视野范围内,可以实现更加逼真的游戏体验。在多人游戏中,需要进行AOI区域视野管理,以避免每个客户端都向服务器请求全部实体数据,导致网络资源消耗过多和服务器压力过大问题。Go语言可以通过多种方式实现AOI区域视野管理,本文将详细介绍如何使用传统的树状结构和切片数据结构两种方式实现AOI区域视野管理。

使用树状结构实现AOI区域视野管理

树状结构是一种描绘对象间层次关系的数据结构,具有根、节点和叶子节点等特性,在AOI区域视野管理中也可以使用树状结构,对实体进行层次划分,以便快速查找和更新实体位置。以下是使用树状结构实现AOI区域视野管理的示例代码:

package main

import (
	"fmt"
	"math"
)

const (
	// 格子的宽度
	gridWidth float64 = 1
	// 格子的高度
	gridHeight float64 = 1
)

// Entity 实体代表一个生物或玩家
type Entity struct {
	id         int     // 实体ID
	x          float64 // 实体坐标x
	y          float64 // 实体坐标y
	gridX      int     // 实体所在的格子x
	gridY      int     // 实体所在的格子y
	isInSight  bool    // 实体是否在玩家视野中
	isInUpdate bool    // 实体是否在更新列表中
}

// Grid 格子代表AOI区域的一个小方格
type Grid struct {
	x       int       // 格子所处的行
	y       int       // 格子所处的列
	players []*Entity // 实体列表
}

// World 世界代表整个AOI区域
type World struct {
	width      int             // 世界的宽度
	height     int             // 世界的高度
	entities   []*Entity       // 所有实体
	gridWidth  float64         // 格子的宽度
	gridHeight float64         // 格子的高度
	grids      map[int]*Grid   // 格子映射表
	gridCount  int             // 格子的数量
	gridQueue  []*Grid         // 等待更新的格子列表
	radius     float64         // 玩家视野半径
	playerSight map[*Entity]int // 玩家视野
}

func newEntity(id int, x, y float64) *Entity {
	entity := &Entity{
		id: id,
		x:  x,
		y:  y,
	}
	entity.updateGridPos()
	return entity
}

func (e *Entity) updateGridPos() {
	e.gridX = int(math.Floor(e.x / gridWidth))
	e.gridY = int(math.Floor(e.y / gridHeight))
}

// NewWorld 创建一个世界
func NewWorld(width, height int, radius float64) *World {
	return &World{
		width:      width,
		height:     height,
		entities:   make([]*Entity, 0),
		gridWidth:  100,
		gridHeight: 100,
		grids:      make(map[int]*Grid),
		gridQueue:  make([]*Grid, 0),
		radius:     radius,
		playerSight: make(map[*Entity]int),
	}
}

// UpdatePlayer 更新玩家位置
func (w *World) UpdatePlayer(id int, x, y float64) {
	entity := w.getEntity(id)
	if entity == nil {
		entity = newEntity(id, x, y)
		w.entities = append(w.entities, entity)
	} else {
		entity.x = x
		entity.y = y
		entity.updateGridPos()
	}
	w.calcEntitySight(entity)
}

// calcEntitySight 计算实体的视野范围
func (w *World) calcEntitySight(entity *Entity) {
	entity.isInSight = false
	if _, ok := w.playerSight[entity]; !ok {
		w.playerSight[entity] = entity.id
	} else {
		return
	}

	gridX, gridY := entity.gridX, entity.gridY
	for i := -1; i <= 1; i++ {
		for j := -1; j <= 1; j++ {
			newGridX, newGridY := gridX+i, gridY+j
			if newGridX >= 0 && newGridX*w.gridWidth <= float64(w.width) && newGridY >= 0 && newGridY*w.gridHeight <= float64(w.height) {
				index := newGridX*w.width + newGridY
				if grid, ok := w.grids[index]; ok {
					for _, other := range grid.players {
						if other.id != entity.id && distance(entity.x, entity.y, other.x, other.y) <= w.radius {
							entity.isInSight = true
							if _, ok := w.playerSight[other]; !ok {
								w.playerSight[other] = other.id
								other.isInSight = true
							}
							break
						}
					}
				}
			}
		}
	}
}

// GetNearbyPlayers 获取附近的玩家
func (w *World) GetNearbyPlayers(id int) []*Entity {
	entity := w.getEntity(id)
	if entity == nil {
		return nil
	}
	players := make([]*Entity, 0)
	gridX, gridY := entity.gridX, entity.gridY
	for i := -1; i <= 1; i++ {
		for j := -1; j <= 1; j++ {
			newGridX, newGridY := gridX+i, gridY+j
			if newGridX >= 0 && newGridX*w.gridWidth <= float64(w.width) && newGridY >= 0 && newGridY*w.gridHeight <= float64(w.height) {
				index := newGridX*w.width + newGridY
				if grid, ok := w.grids[index]; ok {
					for _, player := range grid.players {
						if player.id != entity.id && player.isInSight {
							players = append(players, player)
						}
					}
				}
			}
		}
	}
	return players
}

func distance(x1, y1, x2, y2 float64) float64 {
	return math.Sqrt(math.Pow(x1-x2, 2) + math.Pow(y1-y2, 2))
}

func (w *World) updatePlayerList(id int, isInUpdate bool) {
	entity := w.getEntity(id)
	if entity != nil {
		entity.isInUpdate = isInUpdate
		gridX, gridY := entity.gridX, entity.gridY
		for i := -1; i <= 1; i++ {
			for j := -1; j <= 1; j++ {
				newGridX, newGridY := gridX+i, gridY+j
				if newGridX >= 0 && newGridX*w.gridWidth <= float64(w.width) && newGridY >= 0 && newGridY*w.gridHeight <= float64(w.height) {
					index := newGridX*w.width + newGridY
					if grid, ok := w.grids[index]; ok {
						grid.updatePlayer(entity, isInUpdate)
						if !isInUpdate && !grid.isInUpdateQueue() {
							w.gridQueue = append(w.gridQueue, grid)
							grid.isInUpdate = true
						}
					}
				}
			}
		}
	}
}

func (g *Grid) updatePlayer(entity *Entity, isInUpdate bool) {
	for i, player := range g.players {
		if player.id == entity.id {
			g.players[i].isInUpdate = isInUpdate
			return
		}
	}
	if isInUpdate {
		g.players = append(g.players, entity)
		entity.isInUpdate = true
	} else {
		entity.isInUpdate = false
	}
}

func (g *Grid) isInUpdateQueue() bool {
	for _, grid := range world.gridQueue {
		if grid.x == g.x && grid.y == g.y {
			return true
		}
	}
	return false
}

func (w *World) getEntity(id int) *Entity {
	for _, entity := range w.entities {
		if entity.id == id {
			return entity
		}
	}
	return nil
}

// Refresh 可以在游戏循环中调用
func (w *World) Refresh() {
	// 排序
	for _, entity := range w.entities {
		w.updatePlayerList(entity.id, false)
	}
	// 移除已不可见的玩家
	for _, entity := range w.entities {
		if !entity.isInSight && entity.isInUpdate {
			w.updatePlayerList(entity.id, false)
		}
	}
	// 添加新可见的玩家
	for _, entity := range w.entities {
		if entity.isInSight && !entity.isInUpdate {
			w.updatePlayerList(entity.id, true)
		}
	}
	// 更新等待更新的格子
	for _, grid := range w.gridQueue {
		grid.isInUpdate = false
	}
	w.gridQueue = make([]*Grid, 0)
}

使用切片数据结构实现AOI区域视野管理

除了树状结构之外,还可以使用切片数据结构实现AOI区域视野管理。切片使用更加简单,但是在实现时需要对实体进行位置排序和查找。以下是使用切片数据结构实现AOI区域视野管理的示例代码:

package main

import (
	"math"
	"sort"
)

const (
	// 格子的宽度
	cellWidth float64 = 1
	// 格子的高度
	cellHeight float64 = 1
)

// Player 玩家实体
type Player struct {
	ID          int     // 玩家ID
	X           float64 // 玩家位置X
	Y           float64 // 玩家位置Y
	CellX       int     // 玩家所在的格子X
	CellY       int     // 玩家所在的格子Y
	Neighbors   []*Player
	IsInSight   bool // 是否在视野内
	IsInUpdate  bool // 是否需要更新
	IsInRefresh bool // 是否需要刷新
}

// World AOI世界
type World struct {
	GridWidth  float64           // 格子的宽度
	GridHeight float64           // 格子的高度
	Players    []*Player         // 所有玩家
	Grids      map[int][]*Player // 网格
}

func (w *World) newPlayer(id int, x, y float64) *Player {
	w.Players = append(w.Players, &Player{
		ID:     id,
		X:      x,
		Y:      y,
		IsInUpdate: true,
	})
	return w.Players[len(w.Players)-1]
}

// UpdatePosition 更新玩家的位置
func (w *World) UpdatePosition(id int, x, y float64) {
	player := w.GetPlayerByID(id)
	if player == nil {
		player = w.newPlayer(id, x, y)
	}

	player.X = x
	player.Y = y

	oldCellX := player.CellX
	oldCellY := player.CellY

	player.CellX = int(math.Floor(player.X / w.GridWidth))
	player.CellY = int(math.Floor(player.Y / w.GridHeight))

	if oldCellX != player.CellX || oldCellY != player.CellY {
		player.IsInRefresh = true
	}

	player.IsInSight = false
	w.CalcPlayerAOI(player)
}

// CalcPlayerAOI 计算玩家的视野范围
func (w *World) CalcPlayerAOI(p *Player) {
	p.IsInSight = false
	p.Neighbors = nil

	var centerX, centerY int
	for i, player := range w.Players {
		if player.ID != p.ID && player.IsInSight == false {
			if player.CellX >= p.CellX-1 && player.CellX <= p.CellX+1 &&
				player.CellY >= p.CellY-1 && player.CellY <= p.CellY+1 {
				if distance(p.X, p.Y, player.X, player.Y) <= w.GridWidth {
					player.IsInSight = true
					w.Players[i].IsInSight = true

					p.Neighbors = append(p.Neighbors, player)
					player.Neighbors = append(player.Neighbors, p)

					if _, ok := w.Grids[player.CellX*1000+player.CellY]; !ok {
						w.Grids[player.CellX*1000+player.CellY] = []*Player{}
					}
					w.Grids[player.CellX*1000+player.CellY] = append(w.Grids[player.CellX*1000+player.CellY], p)
				}
			}
		}
	}

	for _, neighbors := range p.Neighbors {
		centerX += neighbors.CellX
		centerY += neighbors.CellY
	}
	if len(p.Neighbors) > 0 {
		centerX /= len(p.Neighbors)
		centerY /= len(p.Neighbors)
	}

	if len(w.Grids[p.CellX*1000+p.CellY]) == 0 && len(p.Neighbors) == 1 {
		neighbors := p.Neighbors[0]
		w.Grids[neighbors.CellX*1000+neighbors.CellY] = nil
		neighbors.Neighbors = []*Player{}
		neighbors.IsInSight = false
		neighbors.IsInUpdate = true
		for i := range w.Players {
			if w.Players[i] != p && w.Players[i] != neighbors {
				w.Players[i].IsInSight = false
				w.Players[i].IsInUpdate = true
			}
		}
	}

	if len(w.Grids[p.CellX*1000+p.CellY]) == 0 {
		return
	}

	sort.Slice(w.Grids[p.CellX*1000+p.CellY], func(i, j int) bool {
		distance1 := distance(p.X, p.Y, w.Grids[p.CellX*1000+p.CellY][i].X, w.Grids[p.CellX*1000+p.CellY][i].Y)
		distance2 := distance(p.X, p.Y, w.Grids[p.CellX*1000+p.CellY][j].X, w.Grids[p.CellX*1000+p.CellY][j].Y)
		return distance1 < distance2
	})

	for i, player := range w.Grids[p.CellX*1000+p.CellY] {
		if distance(p.X, p.Y, player.X, player.Y) <= w.GridWidth {
			player.IsInSight = true
			player.IsInUpdate = true

			if _, ok := w.Grids[player.CellX*1000+player.CellY]; !ok {
				w.Grids[player.CellX*1000+player.CellY] = []*Player{}
			}
			w.Grids[player.CellX*1000+player.CellY] = append(w.Grids[player.CellX*1000+player.CellY], p)
			w.Grids[p.CellX*1000+p.CellY][i] = w.Grids[p.CellX*1000+p.CellY][len(w.Grids[p.CellX*1000+p.CellY])-1]
			w.Grids[p.CellX*1000+p.CellY][len(w.Grids[p.CellX*1000+p.CellY])-1] = p
			w.Grids[p.CellX*1000+p.CellY] = w.Grids[p.CellX*1000+p.CellY][:len(w.Grids[p.CellX*1000+p.CellY])-1]
		}
	}
}

// Refresh 更新所有的分块信息
func (w *World) Refresh() {
	for _, p := range w.Players {
		if p.IsInRefresh || p.IsInUpdate {
			w.CalcPlayerAOI(p)
			p.IsInRefresh = false
			p.IsInUpdate = false
		}
	}
}

// GetPlayerByID 根据ID查询玩家
func (w *World) GetPlayerByID(id int) *Player {
	for _, player := range w.Players {
		if player.ID == id {
			return player
		}
	}
	return nil
}

func distance(x1, y1, x2, y2 float64) float64 {
	return math.Sqrt(math.Pow(x1-x2, 2) + math.Pow(y1