游戏的AOI算法




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


我在实践中所熟知的游戏AOI算法大致有两种,在此做一些总结,顺便梳理一下,打算设计出一套统一的接口封装不同的算法实现(网络上还有些其他算法,因为不熟悉不作记录了)。我所记录的这两种算法也算经典了,一个叫做网格法,一个叫做双链表法。


统一接口设计:

AOI需求大概是这样:
1.游戏地图上有一些npc和玩家在移动,每一个这样移动的对象我们叫做AOIEntity,每一个AOIEntity可以挂多个不同半径的AOI,每一个这种半径的AOI单元我们叫做AOINode,如此,AOIEntity拥有多个AOINode,然后每一个场景管理者AOIManager管理着多个这样的AOIEntity对象。
2.AOI进出事件由三种行为产生:进入场景,离开场景,在场景移动,因为这是AOIEntity相互之间的作用,故因放在AOIManager中统一管理,接口类似这样:
void AOIManager:Enter(AOIEntity *entity, cosnt Point& target_pos);
void AOIManager:Move(AOIEntity *entity, cosnt Point& target_pos);
void AOIManager:Leave(AOIEntity *entity);
3.添加一个AOINode的接口,主要参数是Id(用于标识这个AOI),半径,进出事件的callback函数:
void AOIEntity:AddNode(int aoi_id, float radius, AOICB enter_cb, AOICB leave_cb);
4.获取周围对象和观察者玩家对象集合的接口,这个可以在更上层,通过在响应进出事件的enter_cb, leave_cb中维护这样的集合。


网格算法:

既是把整个场景用网格划分成一个一个小区域(划分粒度可调整),每一个区域是当前场景该区域内的AOIEntity集合,当有一个AOIEntity移动时,根据对象移动之前坐标和目的地坐标,算出移动前所在网格SrcGrid和目的地网格DstGrid,根据一个可调的偏移参数,算出受这次移动影响的各个网格所在的一个网格区域(通常是一个包含这些网格的一个大网格),遍历每一个这样的网格里的每一个AOIEntity,与这个移动AOIEntity互相作比较,主要是比较这些事情:
1.是不是对方曾经在我的一个AOINode的半径内,移动后就不在了,是则产生离开回调;
2.是不是对方曾经不在我的一个AOINode的半径内,移动后就出现了,是则产生进入回调;
注意虽然移动是一个AOIEntity在移动,但是这种比较却要是互相的。
上面说的是网格算法的最简单实现了,当然实践上有许多地方可以优化和调整,包括使用更高效的数据结构,不细说。




双链表算法:

* 此算法名字是自己取的,因为算法基本上就是围绕两个双向链表在转--代表X轴的链表(叫做LinkListX)和代表Y轴的链表(叫做LinkLIstY)。对于每一个AOI单元,以AOIEntity的坐标位置为中心,可以构造出一个AOI矩形(以四元组[xleft,xright,ytop,ybottom]表示)。LinkListX链接的是所有这样的AOI矩形的xleft,xright,LinkListY链接的是所有这样的AOI矩形的ytop,ybottom,并且两者都是按照坐标值从小到大的顺序链接起来的。这样每一个AOI单元都在LinkListX,LinkListY上产生了总共4个节点,特殊的对于每一个可见的AOIEntity,以他们的坐标(XCenter,YCenter)在LinkListX,LinkListY上又产生了总共2个节点。现在当AOIEntity在场景中移动时,他所包含的在LinkList中的节点会相应的更改坐标值,而LinkList为了维护从小到大的顺序,会遍历链表,移动位置,直到重新有序。LinkList在这个过程,会产生AOI事件。
* 具体来说,当AOIEntity要移动到(targetX,targetY), 对应的AOI矩形变成[targetX-R, targetX+R, targetY-R, targetY+R],显然这四个节点值的改变后LinkList不再有序,现在来调整LinkList,可以这样来理解这个过程,对象先在X轴上移动到targetX,对应的是在LinkListX上移动,每次交换两个节点的位置都应该判断:1.两者的拥有者是不是不同的Entity;2.是不是一个是代表Entity的节点,一个是代表AOI矩形边界的节点;3.两者的拥有者整体上能否确实产生AOI进出事件。然后在Y轴上移动到targetY,过程与X轴对称。
* 可以总结一下,LinkList的节点的属性:
struct LinkNode {
byte _type; // 代表类型,主要是区分AOI矩形的边界和Entity本身
AOINode *_owner; // 属于哪个AOI单元,这里把代表Entity本身的节点也当作一个R=0的AOI单元
int _pos_val; // 坐标值,
struct LinkNode *_next, *prev;
}


网格算法原理和实现都简单,每次移动时遍历的受影响的单元是以网格为单位,并不是直接以Entity为单位,会产生许多次无效的遍历,对效率产生多少影响也是依赖网格的划分粒度和场景人数,不过总的来说对于不是海量的对象移动,加上一些上层逻辑相关的优化,一般的MMO已经是够用了。
双链表算法,巧妙的把一个AOI矩形拆成4个不同节点,每次移动遍历的受影响的单元直接是以Entity为单位,省去了许多无效遍历,但是在实现上要较网格算法复杂,另外其性能也是受场景中人数的影响。
### AOI算法在Unity MMORPG中的实现方法 在Unity开发的MMORPG游戏中,AOI(Area of Interest)算法的核心目标是高效管理玩家视野范围内的角色同步,以减少服务器与客户端之间的数据传输量和计算负载。实现这一目标的关键在于如何快速定位并维护玩家视野范围内的其他玩家、NPC或怪物信息,并根据角色状态变化动态更新这些数据。 #### 基于九宫格的AOI划分 一种常见的实现方式是使用**九宫格划分场景**,将整个游戏地图划分为若干个网格,每个网格的大小由AOI范围决定。每个玩家根据其坐标位置被分配到一个特定的网格中。当需要获取某个玩家视野范围内的角色时,只需检查该玩家所在网格及其相邻的8个网格(共9个网格)内的角色[^5]。这种方式的优点是实现简单、查询效率高,适用于大部分MMORPG场景。 #### AOI范围的动态更新 在实际游戏中,玩家的位置是动态变化的,因此需要实时更新玩家所在的网格索引。每当玩家移动时,系统会根据其新位置重新计算其所属的网格,并更新其AOI范围内的角色列表。对于进入或离开AOI范围的角色,服务端会分别通知客户端创建或删除对应的角色对象,从而保证客户端只处理视野范围内的对象[^4]。 #### 状态同步机制 在状态发生变化时(例如玩家移动、攻击等),服务端会优先同步玩家的最新状态(包括位置、方向等),然后再执行后续动作。这种机制确保了客户端接收到的数据始终是服务器上计算出的真实结果,避免了作弊问题。此外,由于所有关键计算都在服务器端完成,客户端只需根据接收到的数据进行渲染和交互处理,进一步降低了客户端的计算压力[^3]。 #### Unity中的实现示例 在Unity中,可以通过以下步骤实现AOI算法: 1. **网格划分**:将游戏地图划分为固定大小的网格,每个网格存储当前处于该网格内的角色列表。 2. **玩家位置更新**:在玩家移动时,根据其新坐标更新其所属的网格索引。 3. **AOI查询**:当需要获取某个玩家视野范围内的角色时,查询其所在网格及其相邻的8个网格内的角色列表。 4. **角色同步**:服务端根据AOI查询结果,将视野范围内的角色状态同步给客户端。 以下是一个简单的网格划分与AOI查询的代码示例: ```csharp public class AOIManager : MonoBehaviour { private Dictionary<Vector2Int, List<GameObject>> grid = new Dictionary<Vector2Int, List<GameObject>>(); private int gridSize = 10; // 网格大小 public void UpdatePlayerGrid(GameObject player) { Vector2Int gridPos = GetGridPosition(player.transform.position); // 从旧网格中移除玩家 foreach (var kvp in grid) { if (kvp.Value.Contains(player)) { kvp.Value.Remove(player); break; } } // 添加到新网格 if (!grid.ContainsKey(gridPos)) { grid[gridPos] = new List<GameObject>(); } grid[gridPos].Add(player); } public List<GameObject> GetPlayersInAOI(Vector3 position) { Vector2Int centerGrid = GetGridPosition(position); List<GameObject> players = new List<GameObject>(); for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { Vector2Int checkGrid = new Vector2Int(centerGrid.x + x, centerGrid.y + y); if (grid.ContainsKey(checkGrid)) { players.AddRange(grid[checkGrid]); } } } return players; } private Vector2Int GetGridPosition(Vector3 worldPos) { int x = Mathf.FloorToInt(worldPos.x / gridSize); int y = Mathf.FloorToInt(worldPos.y / gridSize); return new Vector2Int(x, y); } } ``` #### 相关优化 除了上述基础实现外,还可以通过以下方式进一步优化AOI算法: - **动态网格大小**:根据场景复杂度或玩家密度动态调整网格大小,以平衡查询效率与内存占用。 - **空间分区技术**:引入四叉树(Quadtree)或八叉树(Octree)等空间分区结构,以支持更大范围的AOI查询。 - **事件驱动更新**:采用事件驱动机制,仅在玩家位置发生变化时触发网格更新,减少不必要的计算。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值