Go语言实现AOI区域视野管理流程详解
作者:zhizhizhi志 发布时间:2023-06-20 03:21:51
优化的思路一般是: 第一个是尽量降低向客户端同步对象的数量,第二个是尽量降低单个对象向客户端同步的数据.
"九宫格"是最常见的视野管理算法了.它的优点在于原理和实现都非常简单.
// 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. 流量浪费.有客户端不需要的对象被同步过来了.我们知道它是基于格子来管理地图对象的.那么就会无法保证九宫区域一定刚好是视野范围.肯定是大于视野区域这样才保证同步对象正确.(如果是俯视角那种 ,视野就会是一个 梯形范围.)
或者你可以在服务端中,根据客户端梯形视野在作一遍初筛.
来源:https://blog.csdn.net/qq_16998379/article/details/129288348


猜你喜欢
- 在多线程的使用时,为了线程的顺利进行,我们会使用函数来对某个线程进行暂停运行。在多线程中有两个函数可以实现sleep和wait,不过它们在使
- 页面中无法看见页面,指向的连接网页无法显示 解决方法:1、首先在Dreamweaver中不能中文作为文件名。连目录名也最好是英文的。2、如果
- 今天研究了一下JS的用setAttribute方法实现一个页面两份样式表的效果,具体方法如下:第一步:在连接样式表的元素里定义一个id,例如
- 今天处理了一个日期选择器的ie和ff的兼容问题,本来这种情况就很难找错误,找了好久才把错误定位到js中创建元素的方法document.cre
- 一、Python不同版本的类Python2.2之前是没有共同的祖先的,之后引入Object类,它是所有类的共同祖先类ObjectPython
- Python运行出现DeprecationWarning今天运行 py 时出现下面的问题:DeprecationWarning: Using
- 代码如下:--函数 CREATE function fn_GetPy(@str nvarchar(4000)) returns nvarch
- 在mysql中timestamp数据类型是一个比较特殊的数据类型,他可以自动在你不使用程序更新情况下只要你更新了记录timestamp会自动
- 作为一个Oracle数据库开发者或者DBA,在实际工作中经常会遇到这样的问题:试图对库表中的某一列或几列创建唯一索引时,系统提示ORA-01
- 1.函数调用# 1.调用函数,需要知道函数的名称和参数# 2.调用函数传入的参数需要和函数定义的参数数量和类型一致# 如调用abs函数pri
- 记录win10下安装两个MySQL5.6.35数据库,具体如下环境: OS:window10 DB:MYSQL5.6.35免安装版1.正常安
- 普通滑动验证以http://admin.emaotai.cn/login.aspx为例这类验证码只需要我们将滑块拖动指定位置,处理起来比较简
- 本文的asp xmlhttp类,使用asp的MSXML2.ServerXMLHTTP组件来获取远程音乐文件。类定义 Cls_AspHttp.
- MS SQL性能是很不错的,但是数据库用了一段时间之后,数据库却变得很大,实际的数据量不大。一般都是数据库日志引起的!数据库日志的
- 一、在vs中新建一个工程,然后新建一个源文件二、右击上述图片中的mysql_test工程名,然后在最下方找到属性,并点击三、点击VC++目录
- 有些时间没更新blog了,这两天为了更新<code collection>,于是重写了语法高亮的模块,这次是一个引擎,你可以根据
- 话不多说,直接上代码import copyimport cv2import numpy as npWIN_NAME = 'draw_
- 即使你没听说过“ * 六度分隔理论”,也很可能听过“凯文 • 贝肯 (Kevin Bacon)的六度分隔值游戏”。在这两个游戏中,目标都是
- 在 python2 中,如果想要自定义评价标准的话,可以这么做def cmp(a, b): # 如果逻辑上认为 a < b ,返回 -
- 本文介绍在Python环境中,实现随机森林(Random Forest,RF)回归与各自变量重要性分析与排序的过程。其中,关于基于MATLA