游戏引擎技术分享

性能优化 AOI 客户端 服务端 Entity同步

一、问题背景:为什么需要AOI

在大型多人在线游戏中,一个场景可能同时存在数百个NPC和交互实体。传统的全量同步模型面临严峻的性能挑战:

全量同步模型:所有entity的变化都同步给每个玩家 游戏服务端 玩家1 客户端 玩家2 客户端 玩家3 客户端 处理全部 510 个NPC 处理全部 510 个NPC 处理全部 510 个NPC 每个客户端同步 & 处理所有entity → 性能瓶颈

在这个模型下,服务端每个entity的变化都要广播给所有在线玩家;客户端需要处理所有entity的逻辑运算、网络消息解析和渲染开销。当场景entity数量达到数百个时,网络带宽、CPU Tick、GPU渲染三方面同时承压。

AOI(Area of Interest,感兴趣区域)正是解决这一问题的核心方案。

二、AOI核心原理

2.1 基本概念

AOI的核心思路:以玩家为中心,划定一个圆形区域,只同步该区域内的entity数据。圈外的entity对该玩家"不存在"。

AOI原理:以玩家为圆心,只同步圈内entity 游戏场景俯视图 AOI半径 (R 米) 玩家 NPC 玩家 AOI内NPC (同步) AOI外NPC (不同步) 玩家移动时的进出AOI 进AOI 创建entity 出AOI 销毁entity 进入AOI (创建) 离开AOI (销毁)

图1:AOI基本原理 — 左:静态视角;右:玩家移动时entity进出AOI

关键术语:

  • AOI半径:以玩家为圆心的同步范围,单位为米
  • 进AOI(Enter):entity进入玩家的AOI范围,服务端开始同步该entity数据到客户端,客户端创建entity及其关联的system、模型等
  • 出AOI(Leave):entity离开玩家的AOI范围,服务端停止同步,客户端销毁entity及相关资源

2.2 不同玩家的AOI互相独立

每个玩家都有自己独立的AOI区域,所能"看到"的entity集合各不相同:

不同玩家拥有独立的AOI区域 玩家1 玩家2 玩家1 AOI 玩家2 AOI 两者都可见

图2:玩家1和玩家2各自的AOI区域独立,可见entity集合不同

三、性能提升效果分析

AOI从根本上减少了客户端需要处理的entity数量,带来了网络同步逻辑运算渲染开销三个层面的性能收益:

AOI带来的三层性能收益 网络同步层 Entity数量减少 → RPC调用量降低 → Property同步量降低 → 网络带宽节省 ↓ 带宽 逻辑运算层 IEntity数量减少 → 每帧Tick数量降低 → Gameplay逻辑开销降低 → 动画处理角色减少 ↓ CPU 渲染开销层 可见Primitive减少 → 摄像机裁剪负担降低 → DrawCall数量减少 → 渲染角色数量减少 ↓ GPU

图3:AOI优化从网络、CPU、GPU三个维度同时降低开销

实测数据

510 优化前NPC数量 全量同步
204 常规AOI半径 减少 60%
53 较小AOI半径 减少 90%

数据表明,AOI可以将客户端可见NPC数量降低60%~90%。对于大场景(如开放世界、多人战场),性能收益尤其显著。

四、AOI系统架构设计

4.1 Entity类型分层策略

核心设计决策:只有NPC受AOI影响,其余entity类型(Door、HiddenPoint、玩家、可交互物件等)不受AOI影响,始终全量同步。

这一决策基于以下考量:

  • NPC数量占比最高:场景中NPC通常占entity总数的80%以上,是性能瓶颈的主要来源
  • 玩家之间不能有AOI:多人协作/对抗场景中,玩家之间必须始终互相可见
  • 关键交互物件必须可见:门、交互点等功能性entity不能因AOI而消失,否则影响玩法
Entity类型分层:AOI影响范围 受AOI影响 — 按需同步 NPC 进出AOI动态管理 进AOI → 创建entity + system 出AOI → 销毁entity + system 需要适配状态恢复 需要适配逻辑独立 受限流策略管控 不受AOI影响 — 全量同步 玩家 Door HiddenPoint 可交互物件 始终对所有玩家可见 无需适配进出AOI 无状态恢复开销

图4:Entity类型分层 — NPC受AOI管控,其余类型全量同步

4.2 服务端-客户端AOI交互架构

服务端-客户端AOI交互架构 服务端 (Server) AOI计算模块 Entity管理器 Property / RPC 限流 & 优先级调度器 100ms/个 · 按距离排序 ① 计算玩家位置与各entity距离 ② 判断entity在AOI内/外 ③ 生成进/出AOI事件 ④ 限流排队 ⑤ 按距离优先级下发 ⑥ 同步Property数据 网络传输层 — 进AOI: 全量Property / 出AOI: 销毁通知 客户端 (Client) 创建Entity Property全量 刷新 状态机/Graph 恢复 on_aoi_enter 回调 ▲ 进AOI流程 on_aoi_leave 清理状态 停止逻辑 销毁Entity 释放资源* ▼ 出AOI流程 *优化后对象不实际销毁,复用

图5:AOI系统完整架构 — 从服务端计算到客户端entity生命周期管理

五、适配AOI:核心编程规则

Entity在进出AOI时需要在代码层面做相应处理来保证状态正确恢复,这些处理统称为"适配AOI"。整个适配工作围绕两条核心规则展开:

规则一:客户端逻辑独立

客户端entity的逻辑和表现不能依赖客户端其他entity的状态,必须依赖服务端下发的数据。

因为AOI机制下,其他entity可能随时不在客户端上存在。如果A的逻辑依赖B的状态,而B不在AOI内(客户端没有B的entity),A的逻辑就会出错。

旧方案 — 客户端互相依赖

以扫描技能为例:客户端拿本地NPC数据来计算玩家朝向

问题:目标NPC不在AOI内时,客户端没有该NPC数据,朝向计算失败

新方案 — 逻辑独立

服务端计算朝向,Yaw值通过基础移动方式同步到客户端

客户端只依赖服务端数据,不依赖本地其他entity状态

规则二:适配on_reconnect ≈ 适配AOI

绝大多数情况下,适配好断线重连(on_reconnect)就等同于适配了AOI

原因:on_reconnect时会创建新entity并全量更新property到客户端——entity进入AOI也走同样的流程。两者的客户端处理高度一致:

处理步骤断线重连进入AOI
创建新Entity
Property全量更新
状态机恢复需要需要
Graph恢复需要需要

因此,已有的on_reconnect适配代码通常可以直接覆盖AOI场景。

六、适配实例详解

6.1 成功适配案例

NPC表演动画

旧方案

服务端通过RPC通知客户端push/pop graph

RPC是一次性消息,entity重新进入AOI时不会重放 → 动画丢失

新方案

服务端通过Property控制客户端push/pop graph

Property在entity创建时全量同步 → 客户端可恢复,数据不丢失

电击手机(可交互物件)

手机的逻辑全部运行在服务端,客户端只有动画表现。NPC的动画和逻辑都由服务端数据控制,天然符合"逻辑独立"原则——无需额外适配。

QTE交互

QTE是较为复杂的适配案例,因为涉及两个entity的交互:

  • UI相关操作和客户端动画/逻辑都写在状态机内,已适配on_reconnect
  • 交互双方不依赖彼此,只受服务端数据控制,符合逻辑独立

QTE的额外问题 — loop动画对齐

QTE交互双方的loop动画在进出AOI后可能失去同步。两种解决方案:

1. 服务端记录QTE开始时间,客户端对动画跳帧以对齐时间线

2. 双方都pop旧loop动画再重新push,自然恢复对齐

6.2 未适配的反面案例

BirthPoint(出生点)

客户端UI通过获取所有BirthPoint entity来显示出生点标记。开启AOI后,不在AOI范围内的BirthPoint在客户端不存在,导致出生点UI显示不全。

这是典型的违反"逻辑独立"原则的案例——客户端UI逻辑依赖于"所有同类entity都存在"的假设,AOI下该假设不成立。

七、客户端新增API

为支持AOI机制,客户端Python层新增了以下接口,方便业务逻辑感知和响应AOI事件:

# ClientEntity 和 SystemBase 新增通知函数

class ClientEntity:
    def on_aoi_enter(self):
        """entity进入玩家AOI时被调用"""
        # 在此恢复表现、启动逻辑
        pass

    def on_aoi_leave(self):
        """entity离开玩家AOI时被调用"""
        # 在此清理状态、停止逻辑
        pass

# Area 新增成员变量
class Area:
    is_aoi_enter: bool
    # True  → 当前area在自己的AOI范围内
    # False → 当前area不在自己的AOI范围内

通过这组接口,业务层可以精确控制entity在进出AOI时的行为——比如进入时恢复动画、加载特效,离开时暂停不必要的计算。

八、引入AOI后的实战问题与解决方案

问题1:Entity创建卡顿

现象:entity进入AOI时需要创建entity对象及其关联的system、模型、特效等,这个创建过程造成明显卡顿。

解决方案:预加载 + 对象复用 (空间换时间) Loading阶段 预创建Entity对象 预加载Model 预加载特效 预创建System 放入对象池 → 内存中待命 对象池 (Object Pool) Entity Model System VFX 出AOI不销毁,标记隐藏 进AOI 只创建Entity本身 关联对象直接复用 ✓ 出AOI Entity标记为不可见 对象回收到池中 ✓

图6:预加载 + 对象复用机制 — 用Loading阶段的内存换运行时的流畅度

问题2:高速移动时entity涌入卡顿

现象:被勾中、冲撞等高速移动场景下,短时间内大量entity进入AOI,即使有对象复用,集中创建仍然造成卡顿。

解决方案:服务端限流 + 距离优先级 无限流 ← 瞬间涌入8个entity,严重卡顿 限流后 100ms ← 每100ms下发1个,按距离排序

图7:限流策略将entity创建分散到时间轴上,避免瞬时卡顿

限流规则:

  • 服务端每100ms只允许下发1个entity的进AOI数据
  • 距离排序作为优先级,优先下发最近的entity
  • 玩家感知上是entity从近到远逐步出现,而不是瞬间全部弹出

问题3:技能状态不可恢复

现象:部分技能在断线重连时客户端不会恢复,还会打断当前技能。进入AOI触发entity的on_reconnect,同样存在不恢复问题。

解决:将客户端实现改为可恢复方案——使用状态机或Property驱动。参照QTE案例的做法,确保:

  • 技能状态由Property持久化,不依赖一次性RPC
  • 状态机可以从任意状态正确恢复
  • 客户端不依赖"技能是从头开始播放"的假设

九、AOI扩展机制

除了基础的圆形AOI筛选外,引擎还提供了一系列高级机制,用于应对更复杂的业务场景:

AOI扩展机制一览 基础AOI aoi_exclusive 精确控制哪些avatar 能接收特定entity信息 attention 忽略AOI影响 强制始终同步 dist_aoi 客户端自主控制 是否显示AOI内entity 分层AOI 不同层独立设置 AOI距离 被动AOI entity可被动被发现

图8:基础AOI之上的五种扩展机制

机制核心能力典型场景
aoi_exclusive 修改ID列表,精确控制哪些avatar的客户端能接收到特定entity的信息 Boss战中只让特定队伍看到特定机关
attention 标记entity忽略AOI,始终同步给所有玩家 全局事件NPC、剧情关键角色
dist_aoi 客户端自主控制AOI内entity的显示/隐藏 低配设备进一步裁剪显示量
分层AOI (Layer) 将entity分配到不同层,每层可单独设置AOI距离 重要NPC大AOI半径,路人NPC小AOI半径
被动AOI (Passive) 进场景前设置passive_aoi_radius,其他entity进入该范围时自动同步 隐藏陷阱 — 靠近才显现

十、已知遗留问题

#问题影响
1进入AOI时部分动画恢复不正确NPC表现异常,需要逐一排查动画状态机
2NPC出AOI后胶囊体阴影仍在渲染无用的渲染开销,影响GPU性能
3entity反复进出AOI时 area_impl 数量持续增长潜在内存泄漏,长时间战斗可能导致crash

十一、未来优化方向

AOI算法升级:2D → 3D空间筛选

当前基于2D平面距离筛选,无法区分上下层的entity。升级为3D空间筛选后,可更精确地过滤不同楼层/高度的NPC。甚至可以支持策划在场景中通过标注区域的方式进行辅助筛选。

Entity创建性能深度优化

复用entity对象(而不仅仅是关联资源)、差量化同步进AOI entity的property数据(只发增量而非全量)。优化到位后,限流策略可以适当放宽,提升entity出现速度。

内存精细化管理

当前Entity的system、model等都预加载在客户端内存中。随着场景规模增长,存在内存不足引发crash的风险。未被预加载的entity在进入AOI后加载时还是会卡顿,需要建立更精细的加载/卸载策略。

AOI与模型显隐机制一致性

AOI机制需与模型显示/隐藏机制保持一致,否则可能出现暴露攻方位置等玩法漏洞——因为当前玩家之间没有AOI,但如果NPC的可见性暴露了阵营分布信息,就会被利用。

Graph恢复增强

进入AOI时graph的恢复应考虑数据驱动、动画快进等更健壮的方式,避免动画从头播放导致的不自然表现。

扩大AOI覆盖范围

目前只有NPC受AOI影响。后续可将更多entity类型(如垃圾桶等可交互物件)纳入AOI管理,进一步减少客户端entity数量,压缩不必要的性能开销。