2300 字
12 分钟
视野管理(AOI)——多人游戏中"谁该看到谁"

一、问题:为什么不能把所有数据发给所有人#

你竞拍游戏里目前 4 个玩家,服务端每秒向每个客户端同步所有玩家的状态。假设每条状态更新 200 字节、每秒 10 次:

4 个玩家 × 200 bytes × 10 Hz × 3 个其他玩家 = 24 KB/s per client

完全没问题。

但如果这个游戏变成 100 人大厅呢?

100 × 200 × 10 × 99 = 19.8 MB/s per client ← 一个客户端就要 20MB/s
服务端总出口:100 × 19.8 MB/s ≈ 2 GB/s ← 物理链路上限了

而且大部分数据是垃圾——你在拍卖大厅的东边,完全不需要知道西边 50 米外那 30 个人在做什么。

这就是 AOI(Area of Interest,视野管理) 要解决的问题:对于每个玩家,只同步它”看得见”或”关心”的那部分实体。


二、AOI 的本质#

AOI 是一个空间过滤器。输入是所有实体 + 一个”观察者”,输出是与这个观察者相关的实体子集。

输入:200 个 Player, 1000 个 NPC, 5000 个可交互物体, 观察者 = 玩家A
↓ AOI 过滤
输出:15 个 Player, 50 个 NPC, 100 个可交互物体(玩家A 视野内的)

两个核心指标

  • 精确度:该发的都发了吗(不漏)?不该发的都没发吗(不多发)?
  • CPU 开销:查询本身花了多少时间?每秒要查询 N 次(N = 玩家数 × 同步频率)

三、经典算法一:九宫格(Grid AOI)#

1 基本思想#

把世界划分成等大的格子。每个实体知道自己当前在哪个格子里。查询时,只返回”观察者所在格子 + 周围 8 个邻居格子”里的实体。

┌───┬───┬───┐
│ │ │ │
├───┼───┼───┤
│ │ 我│ │ ← "我"在第 5 格,查询范围是 1-9 全部九格
├───┼───┼───┤
│ │ │ │
└───┴───┴───┘
不相邻的格子里的实体全部跳过。

2 数据结构#

class GridAOI {
// 每格存一组实体
std::unordered_map<GridCoord, std::unordered_set<EntityId>> mGrids;
float mCellSize; // 格子的边长(如 20 米)
public:
void OnEntityMove(EntityId id, Vec3 oldPos, Vec3 newPos) {
GridCoord oldGrid = ToGrid(oldPos);
GridCoord newGrid = ToGrid(newPos);
if (oldGrid != newGrid) {
mGrids[oldGrid].erase(id); // 搬出旧格
mGrids[newGrid].insert(id); // 搬入新格
}
}
std::vector<EntityId> QueryAOI(Vec3 observerPos) {
GridCoord center = ToGrid(observerPos);
std::vector<EntityId> result;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
GridCoord neighbor = { center.x + dx, center.y + dy };
auto it = mGrids.find(neighbor);
if (it != mGrids.end()) {
result.insert(result.end(), it->second.begin(), it->second.end());
}
}
}
return result;
}
};

3 优缺点#

优点缺点
实现极其简单,几十行代码视野必须是矩形——但对大多数俯视角/平坦场景够用
查询 O(1)——只看 9 个格子格子大小难调:太大=过多无关实体;太小=频繁跨格
移动时只有跨格才更新,静止实体零开销高层建筑/垂直空间用 2D 格子搞不定
天然支持”远处的山也看得见”——把大山放进所有格子实体密集区一个格子里几万人,退化

适用于:俯视角、平坦世界、实体密度相对均匀的场景。大部分 MMO 的 AOI 变体都用九宫格或其衍生。


四、经典算法二:十字链表(Cross-Linked List AOI)#

1 基本思想#

不按格子分,而是维护两条有序链表——X 轴一条、Z 轴一条。每个实体同时挂在两条链表中。

查询时,从观察者在 X 链表中的位置出发,向左/向右找,直到遇到距离超过视野范围的实体。同理在 Z 链表里找。取两个结果的交集:

X 链表(按 X 坐标排序):
[实体A: x=5] → [实体B: x=12] → [我: x=15] → [实体C: x=18] → [实体D: x=30]
视野范围 10 米 → 从"我"向左找:B(距离3)、A(距离10,边界)
→ 从"我"向右找:C(距离3)、D(距离15,超出)
X 结果集 = {A, B, C}
Z 链表同理:Z 结果集 = {A, C, E}
最终 AOI = X 结果集 ∩ Z 结果集 = {A, C}

2 优缺点#

优点缺点
视野是精确半径,不是粗糙的九宫格边界插入/删除需要维护两条链表
无格子大小调参问题实体移动时需要在链表中重新定位(链表查找不高效)
密集区不会有一格万人问题实现复杂度高于九宫格

适用于:对 AOI 准确性要求高、实体密度极度不均匀的场景。


五、现代服务器的实际做法#

1 大部分项目用九宫格就够了#

九宫格的”视野边界不精确”在实际业务中通常不是问题——因为策划的体验需求本身就是模糊的(“大概 20 米范围内的玩家”),不是精确数学需求。而且九宫格的简单性带来了更少的 bug 和更好的性能。

不要过度设计。如果九宫格够用,就别上十字链表。

2 混合方案——大部分实际服务器的选择#

静态大世界物体(建筑、地形、NPC)
→ 不通过 AOI 管理,按场景分块走 Streaming
动态玩家
→ 九宫格初筛(极快,去掉 90% 无关玩家)
→ 精确距离检查(剩下 10% 中再过滤)
特殊实体(世界 Boss、活动 NPC、广播事件)
→ 不经过 AOI,直接全图广播或按特殊逻辑分发

3 你的竞拍游戏的特殊性#

竞拍游戏的空间其实不是核心维度——拍卖桌上,空间距离为零,所有玩家都”看得见”对方。这时候 AOI 的含义变成了信息维度上的可见性

原始 AOI:空间距离 → 决定实体是否同步
竞拍游戏的信息 AOI:
├── 公开信息(轮次、倒计时、结算结果)→ 全员同步
├── 半公开信息(出价人数、某角色使用了技能)→ 全员同步,但不暴露具体数值
└── 私密信息(某人的具体出价、某人技能看到的情报)→ 仅自己

这本质上也是 AOI——只是”视野”不是空间距离,而是信息权限。把九宫格的”格子距离”替换成”信息权限等级”,就是竞拍游戏的数据分发模型。


六、UE 的 NetRelevancy 体系#

UE 在网络层内置了基础的 AOI 支持,通过 IsNetRelevantFor 实现:

bool AActor::IsNetRelevantFor(
const AActor* RealViewer, // 实际观看的玩家
const AActor* ViewTarget, // 相机目标
const FVector& SrcLocation // 观察者位置
) const {
// 默认:距离判断
return (GetActorLocation() - SrcLocation).SizeSquared()
< NetCullDistanceSquared;
}

你可以做的事情

// 竞拍游戏里:AuctionResultActor 对所有玩家都是 relevant
bool AAuctionResultActor::IsNetRelevantFor(...) const {
return true; // 结算结果全员可见,不管在哪
}
// 竞拍桌之外的大厅玩家:默认用距离
// 不需要改,UE 默认就是距离相关性
// 角色技能的特殊信息:通过 RPC 定向推送,不靠 Relevancy

UE 的 UReplicationGraph 进一步允许你做更细粒度的控制——指定哪些 Actor 由哪个 ReplicationNode 管理、每个 Node 有不同的同步策略(距离相关、始终同步、按玩家条件同步等)。这是为大项目准备的,你的 mini-game 数量级可能用不上——但理解它存在就好。


七、AOI 与服务端性能#

1 同步频率不是越高越好#

同步频率 = 每客户端每秒同步次数
10 Hz → 适合大部分游戏(每 100ms 一次更新,玩家几乎感知不到)
20 Hz → 竞技游戏(如 CS/Valorant),需要更精确的命中检测
1-5 Hz → 远处/不重要的实体,省带宽

动态频率:近处的玩家 10Hz,远处的 3Hz,更远的 1Hz。九宫格里的距离天然可以做这个分层。

2 增量同步比全量更重要#

AOI 另一面的关键优化是——已经同步过的实体,不需要再次全量发送

实体 A 进入玩家 1 的 AOI:
→ 全量同步 A 的所有属性(位置、装备、状态)
实体 A 在玩家 1 的 AOI 里继续存在:
→ 只同步 A 变化了的属性(位置变了,装备没变)
实体 A 离开玩家 1 的 AOI:
→ 通知玩家 1 "A 消失了"(一个很小的删除包)

这和 UE 的增量复制(Replicated 属性的脏标记)是一脉相承的思路,只是在 AOI 层面加了”进入/离开视野”的生命周期事件。


八、总结#

概念一句话
AOI 问题100 人互相发状态是 O(n²) 带宽,必须过滤
九宫格世界分格子,只发相邻 9 格内的实体——80% 的游戏够用了
十字链表X/Z 双有序链表求交集——更精确但更复杂
混合方案九宫格初筛 + 精确距离过滤 + 特殊实体特殊处理
UE NetRelevancyIsNetRelevantFor + UReplicationGraph
竞拍游戏的信息 AOI不是空间距离,是信息权限等级
动态频率近处 10Hz、远处 1Hz——带宽省一大半

AOI 是服务端最核心的带宽优化手段。理解了它,你的游戏才能在”4 人房”和”100 人大厅”之间伸缩。

视野管理(AOI)——多人游戏中"谁该看到谁"
https://www.m4doka.xyz/posts/engine/engine-12-aoi-visibility/
作者
m4doka
发布于
2026-07-18
许可协议
CC BY-NC-SA 4.0