Go语言实现AOI区域视野管理流程详解

目录
  • 一.定义管理器接口
  • 二.定义区域tower
  • 三.AOI的具体方法实现
    • 1.进入实现
    • 2.离开区域
    • 3.移动
    • 4.同步

优化的思路一般是: 第一个是尽量降低向客户端同步对象的数量,第二个是尽量降低单个对象向客户端同步的数据.

"九宫格"是最常见的视野管理算法了.它的优点在于原理和实现都非常简单.

// 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区域视野管理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang 内存管理简单技巧详解

    目录 引言 预先分配切片 结构中的顺序字段 使用 map[string]struct{} 而不是 map[string]bool 引言 除非您正在对服务进行原型设计,否则您可能会关心应用程序的内存使用情况.内存占用更小,基础设施成本降低,扩展变得更容易/延迟. 尽管 Go 以不消耗大量内存而闻名,但仍有一些方法可以进一步减少消耗.其中一些需要大量重构,但很多都很容易做到. 预先分配切片 数组是具有连续内存的相同类型的集合.数组类型定义指定长度和元素类型.数组的主要问题是它们的大小是固定的——它们

  • Go中的应用配置管理详解

    目录 问题 解决 命令行参数 系统环境变量 打包进二进制文件 配置热更新 开源的fsnotify (1)安装 (2)案例 使用viper开源库实现热更新 问题 Go语言在编译时不会将配置文件这类第三方文件打包进二进制文件中 它既受当前路径的影响,也会因所填写的不同而改变,并非是绝对可靠的 解决 命令行参数 在Go语言中,可以直接通过flag标准库来实现该功能.实现逻辑为,如果存在命令行参数,则优先使用命令行参数,否则使用配置文件中的配置参数. 如下: var ( port string runM

  • Go语言包和包管理详解

    目录 1 包简介 1.1 工作空间 1.2 源文件 1.3 包命名 1.4 main 包 2导包 2.1 两种方式 2.2 包的别名 2.3 简洁模式 2.4非导入模式(匿名导入) 2.5 导包的路径 2.6 远程导入 3 初始化 init 3.1 init总结 4 包管理 4.1 演变过程 4.2 Go Model优点 4.3 启用go module 4.4 GOPROXY 5 go mod详解 5.1 go mod命令 5.2 go.mod说明 5.2.1 依赖的版本 5.2.2 repla

  • Go项目配置管理神器之viper的介绍与使用详解

    目录 1. viper的介绍 2. viper的使用 2.1  Viper对象的创建 2.2 预设一些默认配置 2.3 从命令行工具的选项参数Flags读取 2.4 从环境变量读取 2.5 从配置文件读取 2.6 从远程key/value存储读取 2.7 监听配置变化 2.8 写入配置到文件 3. 源码分析--配置读取的顺序 4. 参考资料 总结 1. viper的介绍 viper是go一个强大的流行的配置解决方案的库.viper是spf13的另外一个重量级库.有大量项目都使用该库,比如hugo

  • Go语言实现AOI区域视野管理流程详解

    目录 一.定义管理器接口 二.定义区域tower 三.AOI的具体方法实现 1.进入实现 2.离开区域 3.移动 4.同步 优化的思路一般是: 第一个是尽量降低向客户端同步对象的数量,第二个是尽量降低单个对象向客户端同步的数据. "九宫格"是最常见的视野管理算法了.它的优点在于原理和实现都非常简单. // AOI 管理器 type AOIManager interface { GetWidth() int GetHeight() int OnEnter(obj scene.GameOb

  • C语言 小游戏打砖块实现流程详解

    始祖是美国英宝格公司(en:Atari Games,ja:アタリ (ゲーム))于1976年推出的街机游戏"Breakout"(en:Breakout),由该公司在1972年发行的"PONG"(en:PONG,ja:ポン (ゲーム),世界上第一款电子游戏,类似台球)改良而来.相较于其前作,一个人就可以玩与变化丰富这两项特点让Breakout相当卖座,使各家公司竞相模仿. 因为规则简单与游戏性,现在许多移动电话都有内建打砖块游戏,也有许多因特网小游戏版本,目前在网上可以

  • 微信小程序MoxB实现全局状态管理流程详解

    目录 安装 MobX 创建 MobX Store 使用 MobX Store 在 Component 构造器中使用 在 Page 页面中使用 github 地址:https://github.com/wechat-miniprogram/mobx-miniprogram-bindings. 安装 MobX 在小程序根目录下执行 npm install --save mobx-miniprogram mobx-miniprogram-bindings 安装 mobx-miniprogram 和 m

  • C语言静态与动态通讯录的实现流程详解

    目录 静态通讯录 contact.h contact.c test.c 动态通讯录 contact.h contact.c qsort.c test.c 本次通讯录的代码已经放到我的Gitee仓库中,感兴趣的小伙伴可以去看看 Gitee 静态通讯录 在我们学习完C语言的结构体.指针以及动态内存管理之后,我们就可以实现一些有意思的小项目了,通过这些小项目可以加深我们对于相关知识的理解. 静态通讯录主要要求有 静态大小,可以记录10个人的信息(大小自己定) 记录的信息如下:名字.性别.年龄.电话.住

  • C语言 数据结构之数组模拟实现顺序表流程详解

    目录 线性表和顺序表 线性表 顺序表 静态顺序表 动态顺序表 代码已经放在Gitee上,需要可以小伙伴可以去看看 用C语言数组模拟实现顺序表 Gitee 线性表和顺序表 线性表 线性表(linear list)是n个具有相同特性的数据元素的有限序列,这是我们广泛使用的数据结构. 线性表在逻辑上是线性结构,也就说是连续的一条直线.但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储. 常见的线性表:顺序表.链表.栈.队列.字符串- 顺序表 顺序表是用一段物理地址连

  • C语言实现的一个三子棋游戏详解流程

    目录 前言 一.三子棋完成程序运行结果 二.三子棋代码实现 1.创建源文件与头文件 2.整体页面的制作 3.制作并打印棋盘 1.在test.c文件中,定义函数game(); 2.在game.h 头文件中 3.在game.c源文件中 4.人机互动下棋 1.在test.c源文件中 2.在game.h头文件中 3.在game.c源文件中 4.此时打印效果 5.判断输赢 1.在test.c源文件中 2.在game.h头文件中 3.在game.c源文件中 4.最终实现结果 最后 前言 三子棋是我们先前所学

  • C语言 风靡一时的黄金矿工游戏实现流程详解

    游戏的玩法主要是通过不断采集地下的黄金和钻石,来得到更高的积分.只有完成任务目标,才可以通过相应的关卡.游戏画面中沙滩上的人物便是玩家的角色,下方深褐色的部分是地下,而黄金和钻石就是玩家需要采集的物品.人物右边的四个方框里的物品是游戏中可以使用的道具. 画面中的虚线就是游戏中的探测器,探测器会不断的左右摆动,当摆动到地下的黄金和钻石的位置时,只需要点击矿坑任意处,便可以发射勘探头采集到这些物品,当然一定要瞄准好再出手呦. 当然想要顺利采集到丰富的资源也不是那么简单的,地下矿坑中,会有各式各样的困

  • 微信小程序支付及退款流程详解

    首先说明一下,微信小程序支付的主要逻辑集中在后端,前端只需携带支付所需的数据请求后端接口然后根据返回结果做相应成功失败处理即可.我在后端使用的是php,当然在这篇博客里我不打算贴一堆代码来说明支付的具体实现,而主要会侧重于整个支付的流程和一些细节方面的东西.所以使用其他后端语言的朋友有需要也是可以看一下的.很多时候开发的需求和相应问题的解决真的要跳出语言语法层面,去从系统和流程的角度考虑.好的,也不说什么废话了.进入正题. 一. 支付 支付主要分为几个步骤: 前端携带支付需要的数据(商品id,购

  • jmeter接口测试教程及接口测试流程详解(全网仅有)

    目录 一.Jmeter简介 二.Jmeter安装 三.设置Jmeter语言为中文环境 四.Jmeter主要元件 五.Jmeter元件的作用域和执行顺序 六.Jmeter进行接口测试流程 七.Jmeter进行接口测试流程步骤详解 八.Jmeter接口测试必定用到的扩展阅读 一.Jmeter简介 Jmeter是由Apache公司开发的一个纯Java的开源项目,即可以用于做接口测试也可以用于做性能测试. Jmeter具备高移植性,可以实现跨平台运行. Jmeter可以实现分布式负载. Jmeter采用

  • Redis Sentinel服务配置流程(详解)

    1.Redis Sentinel服务配置 1.1简介 Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务: 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常. 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过API 向管理员或者其他应用程序发送通知. 自动故障迁移(Automatic failover): 当一个主服务器不

随机推荐