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

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计算编号: g i d = y ∗ c n t s X + y gid=y*cntsX+y gid=ycntsX+y
      • 如何通过x、y计算格子的x、y:
        • 格子的minX: a o i . M i n X + x ∗ g h aoi.MinX+x*gh aoi.MinX+xgh

        • 格子的maxX: a o i . M i n X + ( x + 1 ) ∗ g h aoi.MinX+(x+1)*gh aoi.MinX+(x+1)gh

        • 格子的minY: a o i . M i n Y + y ∗ g l aoi.MinY+y*gl aoi.MinY+ygl

        • 格子的maxY: a o i . M i n Y + ( y + 1 ) ∗ g l aoi.MinY+(y+1)*gl aoi.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])
	}
	// 遍历一个slice
	for _, 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 core

import "fmt"

// AOIManager AOI区域管理模块
type AOIManager struct {
	// 左
	MinX int
	// 右
	MaxX int
	// X方向格子的数量
	CntsX int
	// 上
	MinY int
	// 下
	MaxY int
	// Y方向格子的数量
	CntsY int
	// 当前区域中有哪些格子Id
	Grids 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+x
			gid := y*cntsX + x
			// 初始化gid
			aoi.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])
	}
	// 遍历一个slice
	for _, 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格子id
	gID := m.GetGidByPos(x, y)
	// 通过GID得到周边九宫格信息
	grids := m.GetSurroundGridsByGid(gID)
	// 将九宫格的信息里的全部的Player的id累加到playerIDs
	for _, 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 core

import (
	"fmt"
	"sync"
)

// Grid 一个AOI地图中的格子类型
type Grid struct {
	// 格子ID
	GID 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.kler.cn/a/3163.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java如何手动导jar包

今天用IDEA&#xff0c;需要导入一个Jar包&#xff0c;因为以前都是用eclipse的&#xff0c;所以对这个idea还不怎么上手&#xff0c;连打个Jar包都是谷歌了一下。 但是发现网上谷歌到的做法一般都是去File –> Project Structure中去设置&#xff0c;有没有如同eclipse一样…

HTTP/2.x:最新的网页加载技术,快速提高您的SEO排名

2.1 http2概念HTTP/2.0&#xff08;又称HTTP2&#xff09;是HTTP协议的第二个版本。它是对HTTP/1.x的更新&#xff0c;旨在提高网络性能和安全性。HTTP/2.0是由互联网工程任务组&#xff08;IETF&#xff09;标准化的&#xff0c;并于2015年发布。2.2 http2.x与http1.x区别HTTP…

如何在 Vue 中使用 防抖 和 节流

大厂面试题分享 面试题库前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★地址&#xff1a;前端面试题库 https://mp.weixin.qq.com/s?__bizMzU5NzA0NzQyNg&mid2247485824&idx3&sn70cd26a7c0c683de64802f6cb9835003&scene21#wech…

Flutter GetX 实现 ChatGPT 简单聊天界面

Flutter 是一款跨平台的移动应用开发框架&#xff0c;而 GetX 是 Flutter 中一种简单易用的状态管理和路由管理工具。本篇我们将使用 Flutter 和 GetX 实现一个简单的聊天界面&#xff0c;以与 ChatGPT 进行交互。 我们需要在 Flutter 项目中引入 GetX 库。在pubspec.yaml文件…

学大数据算跟风吗?

随着互联网、物联网和人工智能等技术的不断发展&#xff0c;大数据技术逐渐进入人们的视野&#xff0c;成为一个备受关注的热点话题。那么&#xff0c;大数据专业好学吗&#xff1f;前景如何&#xff1f;下面我们来一起探讨一下。 一、大数据专业的学习难度 大数据技术是一种综…

八大排序算法之归并排序(递归实现+非递归实现)

目录 一.归并排序的基本思想 归并排序算法思想(排升序为例) 二.两个有序子序列(同一个数组中)的归并(排升序) 两个有序序列归并操作代码: 三.归并排序的递归实现 递归归并排序的实现:(后序遍历递归) 递归函数抽象分析: 四.非递归归并排序的实现 1.非递归归并排序算法…

全景丨0基础学习VR全景制作,后期篇第九章:控制点和遮罩工具

本节教程&#xff0c;我们介绍拼接软件PTGui Pro 矫正拉直全景图水平的方法。矫正拉直全景图可以通过后期软件PTGui做到&#xff0c;但我更想跟大家强调的是前期拍摄时&#xff0c;查看水平仪&#xff0c;调平三脚架尤为重要。后期软件强行矫正&#xff0c;不可避免的会因为追求…

蓝桥杯Web前端练习题-----水果拼盘

一、水果拼盘 介绍 目前 CSS3 中新增的 Flex 弹性布局已经成为前端页面布局的首选方案&#xff0c;本题可以使用 Flex 属性快速完成布局。 准备 开始答题前&#xff0c;需要先打开本题的项目代码文件夹&#xff0c;目录结构如下&#xff1a; ├── css │ └── style.…

acm省赛:高桥和低桥(三种做法:区间计数、树状数组、线段树)

题目描述 有个脑筋急转弯是这样的&#xff1a;有距离很近的一高一低两座桥&#xff0c;两次洪水之后高桥被淹了两次&#xff0c;低桥却只被淹了一次&#xff0c;为什么&#xff1f;答案是&#xff1a;因为低桥太低了&#xff0c;第一次洪水退去之后水位依然在低桥之上&#xff…

chatGPT爆火,什么时候中国能有自己的“ChatGPT“

目录 引言 一、ChatGPT爆火 二、中国何时能有自己的"ChatGPT" 三、为什么openai可以做出chatGPT? 四、结论 引言 随着人工智能技术的不断发展&#xff0c;自然语言处理技术也逐渐成为了研究的热点之一。其中&#xff0c;ChatGPT作为一项领先的自然语言处理技术…

PID控制算法详解

1. 前言 PID 即 Proportional&#xff08;比例&#xff09;&#xff0c;Integral&#xff08;积分&#xff09;&#xff0c;Differential&#xff08;微分&#xff09;的英文缩写。顾名思义&#xff0c;PID 控制算法是结合比例&#xff0c;积分和微分三种环节于一体的自动控制…

刷题之最长公共/上升子序列问题

目录 一、最长公共子序列问题&#xff08;LCS&#xff09; 1、题目 2、题目解读 ​编辑 3、代码 四、多写一题 五、应用 二、最长上升子序列问题&#xff08;LIS&#xff09; 1、题目 2、题目解读 3、代码 四、多写一道 Ⅰ、题目解读 Ⅱ、代码 一、最长公共子序列问题&…

Android实时显示时间日期(极简)

Android实时显示时间日期示例图示例图解析TextClock时间控件常用的xml属性及常用方法示例源代码.xml文件.java示例图 示例图解析 格式说明yyyy/MM/dd年月日HH:mm:ss时分秒EEEE星期几EE周几 TextClock时间控件 常用的xml属性及常用方法 属性对应的方法说明android:timeZonese…

7个最好的PDF编辑器,帮你像编辑Word一样编辑PDF

PDF 是具有数字思维的组织的重要交流工具。提供高效的工作流程和更好的安全性&#xff0c;可以创建重要文档并与客户、同事和员工共享。文档的布局已锁定&#xff0c;因此无论在什么设备上查看&#xff0c;格式都保持不变。这是让每个人保持一致的好方法——尤其是那些使用Micr…

JVM垃圾回收机制

文章目录JVM垃圾回收机制如何确定该对象是垃圾引用计数可达性分析如何释放对象常用策略JVM垃圾回收机制 以对象为单位来进行回收 如何确定该对象是垃圾 Java 中使用 可达性分析方法 Python 中时使用 引用计数方法 引用计数 使用额外的计数器&#xff0c;来记录某个对象有多少个…

文件上传的多种利用方式

文件上传的多种利用方式 文件上传漏洞除了可以通过绕过检测进行webshell的上传之外&#xff0c;还有多种其它的漏洞可以进行测试。 XSS漏洞 文件名造成的XSS 当上传任何文件时&#xff0c;文件名肯定是会反显示在网页上&#xff0c;可以使用 XSS Payload做文件名尝试将其上传到…

JeecgBoot 3.5.0 版本发布,开源的企业级低代码平台

项目介绍 JeecgBoot是一款企业级的低代码平台&#xff01;前后端分离架构 SpringBoot2.x&#xff0c;SpringCloud&#xff0c;Ant Design&Vue3&#xff0c;Mybatis-plus&#xff0c;Shiro&#xff0c;JWT 支持微服务。强大的代码生成器让前后端代码一键生成! JeecgBoot引领…

Winform/Csharp中使用StackExchange.Redis连接Redis存取数据并序列化对象/反序列化(支持redis key 模糊搜索)

场景 在winform程序中&#xff0c;需要连接Redis并根据Key进行模糊搜索&#xff0c;对value值进行反序列化为 对象之后进行数据处理和显示。 ServiceStack.redis 这里不使用servicestack.redis&#xff0c;因为这个已经商业化了&#xff0c;会出现每小时6000条数据的限制。…

如何将字符串反转?

参考答案 使用 StringBuilder 或 StringBuffer 的 reverse 方法&#xff0c;本质都调用了它们的父类 AbstractStringBuilder 的 reverse 方法实现。&#xff08;JDK1.8&#xff09;不考虑字符串中的字符是否是 Unicode 编码&#xff0c;自己实现。递归1. public AbstractStrin…

6.网络爬虫——BeautifulSoup详讲与实战

网络爬虫——BeautifulSoup详讲与实战BeautifulSoup简介&#xff1a;BS4下载安装BS4解析对象Tag节点遍历节点find_all()与find()find_all()find()豆瓣电影实战前言&#xff1a; &#x1f4dd;​&#x1f4dd;​此专栏文章是专门针对网络爬虫基础&#xff0c;欢迎免费订阅&#…
最新文章