首頁 > 軟體

Go語言實現AOI區域視野管理流程詳解

2023-03-07 06:01:47

優化的思路一般是: 第一個是儘量降低向用戶端同步物件的數量,第二個是儘量降低單個物件向用戶端同步的資料.

"九宮格"是最常見的視野管理演演算法了.它的優點在於原理和實現都非常簡單.

// AOI 管理器
type AOIManager interface {
	GetWidth() int
	GetHeight() int
	OnEnter(obj scene.GameObject, enterPos *geom.Vector2d) bool
	OnLeave(obj scene.GameObject) bool
	OnMove(obj scene.GameObject, movePos *geom.Vector2d) bool
	OnSync()
}

一.定義管理器介面

1. 進入區域

2. 離開區域

3. 在區域移動

4. 同步資訊

具體實現:

type TowerAOIManager struct {
	minX, maxX, minY, maxY float64 // 單位 m
	towerRange             float64 // 格子大小
	towers                 [][]tower
	xTowerNum, yTowerNum   int
}

劃分格子: 按照實際情況出發,規定格子大小 towerRange. (一般 九個格子的範圍需大於螢幕看到的視野範圍) 這樣才能保證使用者端場景物體的生成和消失在玩家螢幕外.不會突然出現.

// 構造結構
func NewTowerAOIManager(minX, maxX, minY, maxY float64, towerRange float64) AOIManager {
	mgr := &TowerAOIManager{minX: minX, maxX: maxX, minY: minY, maxY: maxY, towerRange: towerRange}
	mgr.init()
	return mgr
}
func (m *TowerAOIManager) init() {
	numXSlots := int((m.maxX-m.minX)/m.towerRange) + 1
	m.xTowerNum = numXSlots
	numYSlots := int((m.maxY-m.minY)/m.towerRange) + 1
	m.yTowerNum = numYSlots
	m.towers = make([][]tower, numXSlots)
	for i := 0; i < numXSlots; i++ {
		m.towers[i] = make([]tower, numYSlots)
		for j := 0; j < numYSlots; j++ {
			key := NewKey(int64(i), int64(j))
			m.towers[i][j].init(int64(key))
		}
	}
}

二.定義區域tower

type tower struct {
	towerId       int64
	context       *TowerSyncContext
	mapId2Obj     map[uint32]scene.GameObject // obj容器
	mapId2Watcher map[uint32]scene.GameObject // 觀察集合
}
func (t *tower) init(key int64) {
	t.towerId = key
	t.context = NewTowerSyncContext() // 同步資訊
	t.mapId2Obj = make(map[uint32]scene.GameObject)
	t.mapId2Watcher = make(map[uint32]scene.GameObject)
}
func (t *tower) AddObj(obj scene.GameObject, fromOtherTower scene.AOITower, bExclude bool) {
	obj.SetAOITower(t)
	t.mapId2Obj[obj.GetId()] = obj
	if fromOtherTower == nil {
		for watcherId, watcher := range t.mapId2Watcher {
			if bExclude && watcherId == obj.GetId() {
				continue
			}
			watcher.OnEnterAOI(obj)
		}
	} else {
		// obj moved from other tower to this tower
		for watcherId, watcher := range fromOtherTower.GetWatchers() {
			if watcherId == obj.GetId() {
				continue
			}
			if _, ok := t.mapId2Watcher[watcherId]; ok {
				continue
			}
			watcher.OnLeaveAOI(obj)
		}
		for watcherId, watcher := range t.mapId2Watcher {
			if watcherId == obj.GetId() {
				continue
			}
			if _, ok := fromOtherTower.GetWatchers()[watcherId]; ok {
				continue
			}
			watcher.OnEnterAOI(obj)
		}
	}
}
func (t *tower) RemoveObj(obj scene.GameObject, notifyWatchers bool) {
	obj.SetAOITower(nil)
	delete(t.mapId2Obj, obj.GetId())
	if notifyWatchers {
		for watcherId, watcher := range t.mapId2Watcher {
			if watcherId == obj.GetId() {
				continue
			}
			watcher.OnLeaveAOI(obj)
		}
	}
}
func (t *tower) addWatcher(obj scene.GameObject, bExclude bool) {
	if bExclude {
		if _, ok := t.mapId2Watcher[obj.GetId()]; ok {
			// todo log
			return
		}
	}
	t.mapId2Watcher[obj.GetId()] = obj
	// now obj can see all objs under this tower
	for neighborId, neighbor := range t.mapId2Obj {
		if neighborId == obj.GetId() {
			continue
		}
		obj.OnEnterAOI(neighbor)
	}
}
func (t *tower) removeWatcher(obj scene.GameObject) {
	if _, ok := t.mapId2Watcher[obj.GetId()]; !ok {
		// todo log
		return
	}
	delete(t.mapId2Watcher, obj.GetId())
	for neighborId, neighbor := range t.mapId2Obj {
		if neighborId == obj.GetId() {
			continue
		}
		obj.OnLeaveAOI(neighbor)
	}
}
func (t *tower) GetWatchers() map[uint32]scene.GameObject {
	return t.mapId2Watcher
}
func (t *tower) GetObjs() map[uint32]scene.GameObject {
	return t.mapId2Obj
}
func (t *tower) GetTowerId() int64 {
	return t.towerId
}
func (t *tower) AddSyncData(mod uint16, cmd uint16, msg protoreflect.ProtoMessage) {
	t.context.AddSyncData(mod, cmd, msg)
}
func (t *tower) Broadcast() {
	if len(t.context.fights) == 0 {
		return
	}
	// 廣播協定
	 ....   
	t.context.ClearContext()
}

三.AOI的具體方法實現

我們在回過頭來繼續說 mgr 的方法.

1.進入實現

前提:

GameObject : 一切場景物體的基礎介面

type GameObject interface {}

Vector2d : X,Y 座標

type Vector2d struct {
	x, y, w float64
}

具體實現:

如果是從上一個區域內離開,則先走 離開上一個區域,然後計算當前進入位置座標對應的九宮區域,

然後把obj 加入到各個區域內

func (m *TowerAOIManager) OnEnter(obj scene.GameObject, enterPos *geom.Vector2d) bool {
	if obj.GetAOITower() != nil {
		m.OnLeave(obj) // 離開上一個區域
	}
	obj.SetPosition(enterPos) // 設定當前位置
    // obj 視野範圍內的所有區域
	m.visitWatchedTowers(enterPos, obj.GetViewRange(), func(tower *tower) {
		tower.addWatcher(obj, false)
	})
	t := m.getTowerXY(enterPos)
    // 當前位置所在的區域
	t.AddObj(obj, nil, false)
	return true
}
func (m *TowerAOIManager) getTowerXY(xyPos *geom.Vector2d) *tower {
	xi, yi := m.transXY(xyPos.GetX(), xyPos.GetY())
	return &m.towers[xi][yi]
}

關鍵的方法:

計算obj當前位置中,視野內能被觀察到的所有區域.

func (m *TowerAOIManager) visitWatchedTowers(xyPos *geom.Vector2d, aoiDistance float64, f func(*tower)) {
	ximin, ximax, yimin, yimax := m.getWatchedTowers(xyPos.GetX(), xyPos.GetY(), aoiDistance)
	for xi := ximin; xi <= ximax; xi++ {
		for yi := yimin; yi <= yimax; yi++ {
			tower := &m.towers[xi][yi]
			f(tower)
		}
	}
}
func (aoiman *TowerAOIManager) getWatchedTowers(x, y float64, aoiDistance float64) (int, int, int, int) {
	ximin, yimin := aoiman.transXY(x-aoiDistance, y-aoiDistance)
	ximax, yimax := aoiman.transXY(x+aoiDistance, y+aoiDistance)
	return ximin, ximax, yimin, yimax
}
func (m *TowerAOIManager) transXY(x, y float64) (int, int) {
	xi := int((x - m.minX) / m.towerRange)
	yi := int((y - m.minY) / m.towerRange)
	return m.normalizeXi(xi), m.normalizeYi(yi)
}
func (m *TowerAOIManager) normalizeXi(xi int) int {
	if xi < 0 {
		xi = 0
	} else if xi >= m.xTowerNum {
		xi = m.xTowerNum - 1
	}
	return xi
}
func (m *TowerAOIManager) normalizeYi(yi int) int {
	if yi < 0 {
		yi = 0
	} else if yi >= m.yTowerNum {
		yi = m.yTowerNum - 1
	}
	return yi
}

2.離開區域

func (m *TowerAOIManager) OnLeave(obj scene.GameObject) bool {
	obj.GetAOITower().RemoveObj(obj, true) // 離開當前區域
    // 查詢視野內所有區域,然後從關注列表中移除
	m.visitWatchedTowers(obj.GetPosition(), obj.GetViewRange(), func(tower *tower) {
		tower.removeWatcher(obj)
	})
	return true
}

3.移動

每幀移動座標點 movePos

func (m *TowerAOIManager) OnMove(obj scene.GameObject, movePos *geom.Vector2d) bool {
	oldX, oldY := obj.GetPosition().GetX(), obj.GetPosition().GetY()
	obj.SetPosition(movePos) //設定當前座標
	t0 := obj.GetAOITower()
	t1 := m.getTowerXY(movePos)
    // 判斷移動是否跨區域了
	if t0.GetTowerId() != t1.GetTowerId() {
		t0.RemoveObj(obj, false)
		t1.AddObj(obj, t0, true)
	}
    // 計算前後變化的區域,進行移除和新增關注列表
	oximin, oximax, oyimin, oyimax := m.getWatchedTowers(oldX, oldY, obj.GetViewRange())
	ximin, ximax, yimin, yimax := m.getWatchedTowers(movePos.GetX(), movePos.GetY(), obj.GetViewRange())
	for xi := oximin; xi <= oximax; xi++ {
		for yi := oyimin; yi <= oyimax; yi++ {
			if xi >= ximin && xi <= ximax && yi >= yimin && yi <= yimax {
				continue
			}
			tower := &m.towers[xi][yi]
			tower.removeWatcher(obj)
		}
	}
	for xi := ximin; xi <= ximax; xi++ {
		for yi := yimin; yi <= yimax; yi++ {
			if xi >= oximin && xi <= oximax && yi >= oyimin && yi <= oyimax {
				continue
			}
			tower := &m.towers[xi][yi]
			tower.addWatcher(obj, true)
		}
	}
	return true
}

4.同步

每影格同步化所有區域變化的物體物件

func (m *TowerAOIManager) OnSync() {
	for i := 0; i < m.xTowerNum; i++ {
		for j := 0; j < m.yTowerNum; j++ {
			m.towers[i][j].Broadcast()
		}
	}
}

簡單的實現了 AOI 區域變化管理,當然後面還需要優化,我們知道"九宮格" 演演算法的缺點:

1 . 當玩家跨越格子的時候,比如說從A點到B點.瞬間會有新增格子,那其中的物件就會進入視野,與此同時,就會有消失的格子,那其中的物件就要消失視野.這個瞬間就會出現一個流量激增點,它可能會導致使用者端卡頓等問題.

2. 流量浪費.有使用者端不需要的物件被同步過來了.我們知道它是基於格子來管理地圖物件的.那麼就會無法保證九宮區域一定剛好是視野範圍.肯定是大於視野區域這樣才保證同步物件正確.(如果是俯視角那種 ,視野就會是一個 梯形範圍.)

或者你可以在伺服器端中,根據使用者端梯形視野在作一遍初篩.

到此這篇關於Go語言實現AOI區域視野管理流程詳解的文章就介紹到這了,更多相關Go AOI區域視野管理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com