如果对Unity其他的优化方式感兴趣,可以看我之前写的另外一篇文章详细讲了各种优化方法,包括但不限于CPU、GPU、资源优化等。

概念与设计原则

  1. 例子:
    1. 一个地图中,如果有100人,每个人都移动一下,那么服务器就需要同步1w次同步消息
    2. 且实际情况下,每人每秒不可能只同步一次,一般情况下可能会每人1s会发送10次同步消息,就需要10w次消息处理
    3. 按照经验,平均每个消息有30b,这就需要3MB/s的传输速度,需要24兆的带宽,且服务器的交换机的转发率可能甚至无法应对那么多的发包量(或者成本太高),这显然不合理,因此需要引入AOI来优化
  2. AOI:Area of interest,感兴趣的范围
    1. 建立兴趣范围清单
    2. 只对兴趣范围的目标广播
    3. 极大的降低消息处理压力和网络负载
  3. 广播的范围:
    1. 全服广播(全服公告、全服大喇叭等)
    2. 地图广播(移动同步、战斗同步等),频率最高,压力最大,因此主要在此引入AOI
    3. 社交关系(公会、好友、队伍等)
    4. 交互目标(加好友、挑战、交易等)
    5. 玩家自身(登录,接/交任务,获得道具)
  4. 设计原则:
    1. 分析核心需求:
      1. 降低压力消耗
      2. 降低带宽
      3. 提高负载
    2. 明确设计目标
      1. 设计兴趣范围规划方案
      2. 设计对应的对象与数据结构
      3. 得到高性能算法
    3. 不为设计而设计
      1. 优化思想
      2. 忘记技术
      3. 不忘初心:适可而止,避免过度优化,优化的曲线通常是对数函数

范围区域划分方案

  1. 每个玩家设置一个以自己为圆心的范围,只需要广播自身信息给自身周围中范围内的目标即可,如下图(理解红圈和篮圈需要引入下面的层级设计)。image-20221001195430541
  2. AOI的级别设定:
    1. 比如Level3级别中的目标快要进入视野了,预先加载部分的的资源,方便后续进入Level2、1时候使用
    2. Level2中的目标已经进入了玩家的视野,需要把他的外观(比如皮肤、武器等等)等基本数据加载到屏幕空间内,同步给玩家,但是同步不需要那么精准
    3. Level1中的目标不仅进入了玩家的视野而且已经离得很近了,需要把玩家动作、战斗情况等状态加载出来,同步给玩家,同步需要十分精准
    4. image-20221001195737836
    5. image-20221001200354166
    6. //伪代码
      void OnEntityMove(who)
      {
          foreach(var entity in entities)
          {
              //如果是自己,跳过
              if(who == entity) continue;
              bool nowInAOI = who.Distance(entity) < who.AOIRange;
              bool alreadyInAOI = who.AOI.contains(entity);
              if(alreadyInAOI && !nowInAOI)
              {
                  //互相移除出AOI列表
                  who.onLeaceAOI(entity);
                  entity.onLeaceAOI(who);
              }
              if(!alreadyInAOI && nowInAOI)
              {
                  //互相增加进AOI列表
                  who.onEnterAOI(entity);
                  entity.OnEnterAOI(who);
              }
          }
      }
      
    7. 优缺点:
      1. 优点
        1. 不需要实现特殊的数据结构
        2. 易于实现
      2. 缺点:
        1. 给个人需要和每个人进行AOI范围判断,计算成本较高(1+N)*N/2
        2. 即1000人需要500500次
    8. 改善方案:
      1. 多线程:并行计算,提高计算效率
      2. 延迟计算:减少AOI的判断计算间隔,比如每秒才跑一次
      3. 分批计算:100/Frame,每帧只跑一部分的目标

网格区域划分方案

  1. 引入网格的概念,你在哪个格子只同步格子内的目标,如下图:
    1. image-20221001202433061
  2. //伪代码
    void OnEntityMove(who)
    {
        //获取当前所在的格子
        int new_x = (int)(who.position.x/size);
        int new_y = (int)(who.position.y/size);
        //进入了新的格子
        if(new_x != who.grid_x || new_y != who)
        {
            who.LeaveGrid(who.grid_x, who.grid_y);
            who.EnterGrid(new_x, new_y);
            this.grid_x = new_x;
            this.grid_y = new_y
        }
    }
    
  3. 优缺点:
    1. 优点:计算速度快
    2. 缺点:
      1. 需要额外的数据结构存储格子信息
      2. 需要额外的格子管理逻辑
      3. 实现复杂度高
      4. 格子边界问题
  4. 改善方法:
    1. 格子边界问题:
      1. AOI的范围增加未当前玩家周围的九宫格
      2. 增加入口边界和出口边界让各自边界更加松散
    2. 算法优化:
      1. 优化数据结构:四叉树、八叉树、BVH树等
      2. 降低运算消耗
    3. ECS架构:采用面向数据概念的优化架构,提高运算性能
    4. 并行运算与GPU加速:
      1. 利用并行运算提升性能
      2. 采用GPU加速减少CPU消耗

AOI优化方案参考

  1. 背景:由于正在开发的游戏涉及到10W个移动角色,如果单服1W玩家的话,采用双向循环查找,那就是10E的量级,不得不对算法做优化。
  2. 场景:1000*1000的地图,1W客户端角色,两个角色间距离是10时有效
  3. 优化前:随机生成1W角色的位置信息,然后计算哪些角色的信息需要发给范围内的客户端,使用最简单的双向查找算法,找到40030个有效值,耗时880ms
  4. 优化1:代码逻辑优化,位置是双向的,也就是A在B的范围内,B也在A的范围内,因此只需要循环n*(n-1)/2次,结果耗时416ms
  5. 优化2:网格优化,由于大部分的角色位置相距较远,因此对地图进行分区,以100为单位,整个地图被分成100个区域,创建区域数组Player[100][],然后计算每个客户端的更新范围所在的区域,并将客户端加入到区域中,注意客户端的四个顶点可能在不同的区域上, 此时在几个区域就要加入几个区域。最后计算角色所在区域,并和区域内的Player计算距离。此算法得到的结果是22.5ms。
  6. 优化3:位运算优化,区域大小用2^n来表示,从而在计算角色所在区域时可以用位运算左移右移来处理,使用64作为区域大小,优化后的平均耗时是15.5ms
  7. 优化4:优化网格,从算法的耗时来看,区域小一些,则区域数量变多,但每个区域的角色数量就少了,需要计算的量也会变少,使用32作为区域大小后耗时为9.5ms
  8. 优化5:如果地图变大一些,角色更加稀疏,则计算量会更少,使用10000*10000的地图,同样是64大小的区域耗时为1.8ms

拓展阅读

空间数据结构(四叉树/八叉树/BVH树/BSP树/k-d树) - KillerAery - 博客园

碰撞檢測的優化-四叉樹(Quadtree)

游戏中的AOI(Area of Interest)算法

游戏服务器的场景管理计算AOI终于搞出一个靠谱的方案了。。。