基于Golang实现多人在线游戏的AOI算法

news/2024/11/27 8:44:46/

1、AOI基本介绍

游戏的AOI(Area of Interest)算法应该算作游戏的基础核心了,许多逻辑都是因为AOI进出事件驱动的,许多网络同步数据也是因为AOI进出事件产生的。因此,良好的AOI算法和基于AOI算法的优化,是提高游戏性能的关键。

为此,需要为每一个玩家设置一个AOI,当一个对象状态发生改变时,需要将信息广播给全部玩家,那些AOI覆盖到的玩家会收到这条广播消息,从而做出对应的响应状态。

功能:

  • 服务器的玩家或NPC状态发生变化时,将消息广播到附近的玩家。
  • 玩家进入NPC警戒区域时,AOI模块将消息发送给NPC,NPC再作出响应的AI反应。

2、网格法实现AOI算法

首先绘制一个2D的地图
请添加图片描述

假设在8中的玩家抽中了一把武器,那么周围2、3、4、9、14、13、12、7方格内的玩家都应该收到消息。通过分析,我们至少脑海里要有两个结构体,第一就是AOI格子数据类型,第二就是AOI管理格子(地图)数据类型。

格子的详细情况:

  • 格子
    • 属性:格子ID、格子的右边界坐标、格子左边界坐标、格子上边界坐标、格子下边界坐标、当前格子内玩家/物体成员的ID集合、保护锁
    • 方法:初始化格子的方法、格子添加玩家/物品、格子删除玩家/物品、获取所有玩家、打印格子信息(调试)
  • 管理格子(地图)
    • 属性:区域的左边界、区域的右边界、X方向格子的数量、区域的上边界、区域的上边界、Y方向格子的数量、当前区域有哪些格子map

    • 方法:初始化一个AOI区域管理模块、打印当前AOI地图的信息(调试)、根据格子ID查询周围的格子信息、添加一个玩家到指定格子中、移除一个格子中某个玩家、通过坐标将玩家添加进一个格子中、通过坐标把一个玩家从指定的格子中移除、通过玩家的坐标获得当前player周边九宫格内全部的玩家、通过坐标获取得到对应的玩家所在的GID

      • 如何通过x、y计算编号:gid=y∗cntsX+ygid=y*cntsX+ygid=ycntsX+y
      • 如何通过x、y计算格子的x、y:
        • 格子的minX:aoi.MinX+x∗ghaoi.MinX+x*ghaoi.MinX+xgh

        • 格子的maxX:aoi.MinX+(x+1)∗ghaoi.MinX+(x+1)*ghaoi.MinX+(x+1)gh

        • 格子的minY:aoi.MinY+y∗glaoi.MinY+y*glaoi.MinY+ygl

        • 格子的maxY:aoi.MinY+(y+1)∗glaoi.MinY+(y+1)*glaoi.MinY+(y+1)gl

          请添加图片描述
          完整代码在文章的最后。

3、实现通知周围

如果是黄色格子里面对象,我们如何实现通知周围的格子呢?其主要情况有以下几种:
请添加图片描述
当然在这里我们可以分别格子是不是内部点或者顶点或者是边缘点,但是这样算法复杂程度有些复杂了。在这里我们的采用都按照第一种来,如果你的周围是合法的格子就直接返回,而那些不合法的格子就直接不要。算法实现细节如下:

// GetSurroundGridsByGid 根据格子GID得到周边就宫格的ID集合
func (m *AOIManager) GetSurroundGridsByGid(gID int) (grids []*Grid) {// 判断gID是否在AOIManager中if _, ok := m.Grids[gID]; !ok {return nil}// 初始化返回值数组grids = append(grids, m.Grids[gID])// 判断gID左边是否有格子、右边是否有格子indexX := gID % m.CntsX// 需要通过gID得到当前格子X轴的编号 idx:= id % cnx// 判断idx编号坐标右边是否还有格子if indexX > 0 {grids = append(grids, m.Grids[gID-1])}// 判断idx编号坐标左边是否还有格子if indexX < m.CntsX-1 {grids = append(grids, m.Grids[gID+1])}// 遍历一个slicefor _, grid := range grids {if grid.GID/m.CntsY > 0 {grids = append(grids, m.Grids[grid.GID-5])}if grid.GID/m.CntsY < m.CntsY-1 {grids = append(grids, m.Grids[grid.GID+5])}}return
}

4、完整代码

aoi.go

package coreimport "fmt"// AOIManager AOI区域管理模块
type AOIManager struct {// 左MinX int// 右MaxX int// X方向格子的数量CntsX int// 上MinY int// 下MaxY int// Y方向格子的数量CntsY int// 当前区域中有哪些格子IdGrids map[int]*Grid
}// NewAOIManager 初始化一个AOI区域管理模块
func NewAOIManager(minX, maxX, cntsX, minY, maxY, cntsY int) *AOIManager {aoi := &AOIManager{MinX:  minX,MaxX:  maxX,CntsX: cntsX,MinY:  minY,MaxY:  maxY,CntsY: cntsY,Grids: make(map[int]*Grid),}// 给aoi初始化区域中所有的格子进行编号和初始化gh := aoi.gridHeight()gl := aoi.gridLength()for y := 0; y < cntsY; y++ {for x := 0; x < cntsX; x++ {/*这里是关键*/// 根据x,y编号,计算格子ID:idy*cntsX+xgid := y*cntsX + x// 初始化gidaoi.Grids[gid] = NewGrid(gid, aoi.MinX+x*gh,aoi.MinX+(x+1)*gh,aoi.MinY+y*gl,aoi.MinY+(y+1)*gl)}}return aoi
}// 得到每个格子在X轴方向的宽度
func (m *AOIManager) gridHeight() int {return (m.MaxX - m.MinX) / m.CntsX}// 得到每个格子在y轴方向的长度
func (m *AOIManager) gridLength() int {return (m.MaxY - m.MinY) / m.CntsY
}// 打印格子的信息
func (m *AOIManager) String() string {// 打印aoi信息s := fmt.Sprintf("AOIManager:\n"+"MinX:%d,MaxX:%d,CntsX:%d\n"+"MinY:%d,MaxX:%d,CntsX:%d\n", m.MinX, m.MaxX, m.CntsX, m.MinY, m.MaxY, m.CntsY)// 打印格子的信息for _, grid := range m.Grids {s += fmt.Sprintln(grid)}return s
}// GetSurroundGridsByGid 根据格子GID得到周边就宫格的ID集合
func (m *AOIManager) GetSurroundGridsByGid(gID int) (grids []*Grid) {// 判断gID是否在AOIManager中if _, ok := m.Grids[gID]; !ok {return nil}// 初始化返回值数组grids = append(grids, m.Grids[gID])// 判断gID左边是否有格子、右边是否有格子indexX := gID % m.CntsX// 需要通过gID得到当前格子X轴的编号 idx:= id % cnx// 判断idx编号坐标右边是否还有格子if indexX > 0 {grids = append(grids, m.Grids[gID-1])}// 判断idx编号坐标左边是否还有格子if indexX < m.CntsX-1 {grids = append(grids, m.Grids[gID+1])}// 遍历一个slicefor _, grid := range grids {if grid.GID/m.CntsY > 0 {grids = append(grids, m.Grids[grid.GID-5])}if grid.GID/m.CntsY < m.CntsY-1 {grids = append(grids, m.Grids[grid.GID+5])}}return
}// GetPidsByPos 通过横纵坐标得到周边9宫格内全部的PlayersIDs
func (m *AOIManager) GetPidsByPos(x, y float32) (playerIDs []int) {// 得到当前玩家的GID格子idgID := m.GetGidByPos(x, y)// 通过GID得到周边九宫格信息grids := m.GetSurroundGridsByGid(gID)// 将九宫格的信息里的全部的Player的id累加到playerIDsfor _, grid := range grids {playerIDs = append(playerIDs, grid.GetPlayerIDs()...)fmt.Printf("========> grid ID:%d ,pid:%v <===========", grid.GID, grid.GetPlayerIDs())}return
}// GetGidByPos 通过x、y横纵轴坐标得到当前的GID格子编号
func (m *AOIManager) GetGidByPos(x, y float32) int {idx := (int(x) - m.MinX) / m.gridLength()idy := (int(y) - m.MinY) / m.gridLength()return idy*m.CntsX + idx
}// AddPidToGrid 添加一个PlayerID到一个格子中
func (m *AOIManager) AddPidToGrid(pID, gID int) {m.Grids[gID].Add(pID)
}// RemovePidFromGrid 移除一个格子中的PlayerID
func (m *AOIManager) RemovePidFromGrid(pID, gID int) {m.Grids[gID].Delete(pID)
}// GetPidsByGid 通过GID获得全部的PlayerID
func (m *AOIManager) GetPidsByGid(gID int) (playerIDs []int) {playerIDs = m.Grids[gID].GetPlayerIDs()return
}// AddToGridByPos 通过坐标将Player添加到一个格子中
func (m *AOIManager) AddToGridByPos(pID int, x, y float32) {gID := m.GetGidByPos(x, y)grid := m.Grids[gID]grid.Add(pID)
}// RemoveFromGridByPos 通过坐标把一个Player从一个格子中删除
func (m *AOIManager) RemoveFromGridByPos(pID int, x, y float32) {gID := m.GetGidByPos(x, y)grid := m.Grids[gID]grid.Delete(pID)
}

grid.go

package coreimport ("fmt""sync"
)// Grid 一个AOI地图中的格子类型
type Grid struct {// 格子IDGID int// 左界MinX int// 右界MaxX int// 上界MinY int// 下界MaxY int// 当前玩家集合playerIDs map[int]bool// 保护锁pIDLock sync.RWMutex
}// NewGrid 初始化格子的方法
func NewGrid(id, minX, maxX, minY, maxY int) *Grid {return &Grid{GID:  id,MinX: minX,MaxX: maxX,MinY: minY,MaxY: maxY,}
}// Add 格子添加玩家/物品
func (g *Grid) Add(playerID int) {g.pIDLock.Lock()defer g.pIDLock.Unlock()g.playerIDs[playerID] = true}// Delete 格子删除玩家/物品
func (g *Grid) Delete(playerID int) {g.pIDLock.Lock()defer g.pIDLock.Unlock()delete(g.playerIDs, playerID)
}// GetPlayerIDs 获取所有玩家ID
func (g *Grid) GetPlayerIDs() (ids []int) {g.pIDLock.Lock()defer g.pIDLock.Unlock()for k, _ := range g.playerIDs {ids = append(ids, k)}return
}// 打印格子信息(调试)
func (g *Grid) String() string {return fmt.Sprintf("Grid id:%d,minX:%d,"+"maxX:%d,minY:%d,"+"maxY:%d,playerIDs:%v\n", g.GID, g.MinX, g.MaxX, g.MinY, g.MaxY, g.playerIDs)}

http://www.ppmy.cn/news/35009.html

相关文章

ubuntu下使用GCC开发单片机的过程

以下是一个简单的单片机C程序示例,实现的功能是控制LED灯的闪烁: #include <reg52.h> // 导入单片机的寄存器定义void main() {while(1) { // 无限循环P1 = 0x00; // P1口输出低电平delay(1000); // 延时1秒P1 = 0xff; // P1口输出高电平delay(1000); // 延时1秒…

[刷题]背包问题

递归问题特性 ①问题有最优子结构&#xff1a;问题存在最优解&#xff0c;且与其子问题最优解重合 ②无后效&#xff1a;前后状态值只和值本身有关&#xff0c;和问题无关。 解决思路&#xff1a; ①将原问题分解为子问题 ②确定状态 ③确定初始状态值 ④确定状态转移方程&…

b01lers(php.galf)

目录 前文 正文 前文 <?phpclass A{public $codeNULL;public $argsNULL;public function __construct($code,$argsNULL){$this->code$code;$this->args$args;print_r("2333") ;} public function __invoke($code,$args){echo $code;print_r("执行inv…

[入门必看]数据结构2.3:线性表的链式表示

[入门必看]数据结构2.3&#xff1a;线性表的链式表示第二章 线性表2.3 线性表的链式表示知识总览2.3.1 单链表的定义2.3.2_1 单链表的插入删除2.3.2_2 单链表的查找2.3.2_3 单链表的建立2.3.3 双链表2.3.4 循环链表2.3.5 静态链表2.3.6 顺序表和链表的比较2.3.1 单链表的定义单…

【3.24】Mybatis常见面试题

Mybatis常见面试题 #{}和&#xffe5;{}的区别是什么&#xff1f; 【#】&#xff1a;底层执行SQL使用PreparedStatement对象&#xff0c;预编译SQL&#xff0c;相对安全。入参使用占位符的方式。 【$】&#xff1a;底层执行SQL使用Statement对象&#xff0c;入参使用SQL拼接的…

upload-lab通关

1.pass-1 前端js检查&#xff0c;修改js或修改后缀绕过查看源代码&#xff0c;可以发现是客户端的进行检查:两种方法&#xff1a;(1).禁用js或修改js代码(2).抓包修改后缀这里采用抓包修改后缀先改成可以上传的后缀名&#xff0c;在抓包修改后缀名为php1.php代码<?php phpi…

特殊的LaTex数学符号

LaTeX符号说明\partial∂\partial∂\DeltaΔ\DeltaΔ\betaβ\betaβ\piπ\piπ\alphaα\alphaα\thetaθ\thetaθ\xiξ\xiξ\varphiφ\varphiφ\times\times乘\div\div除\dfrac{dz}{dx}dzdx\dfrac{dz}{dx}dxdz​分号\neq≠\neq不等于\approx≈\approx≈约等于\equiv≡\equiv≡…

【华为OD】几何平均值最大子数组_ [二分查找+前缀和]

目录 一. 🌟 题目描述二. 🌟 输入描述三. 🌟 输出描述3.13.2 用例四. 🌟 题目解析五. 🌟 Java玩法六. 🌟 JavaScript玩法一. 🌟 题目描述 从一个长度为 N 的正数数组 numbers 中找出长度至少为 L 且几何平均值最大子数组,并输出其位置和大小。(K 个数的几何平均…