游戏设计 MMORPG类九宫格视野

本文探讨了在MMORPG游戏中,如何通过九宫格视野设计优化玩家视野更新,以减少不必要的消息推送,提升游戏性能。文章介绍了以玩家为中心的视野扩展方式,地图格子划分,以及玩家移动时视野刷新的策略,强调了在大规模PVP场景中视野筛选的重要性,并提出了基于优先级的视野规则来确保体验和性能的平衡。
摘要由CSDN通过智能技术生成


背景

玩家视野设计背景

      AOI(Area of Interest),即感兴趣区域。可以看成在游戏中玩家在所在场景中实时看到的区域,即AOI会随着玩家的移动而改变。在游戏中,会有很多个场景(地图),每个地图中都有很多玩家或者npc,视野数据是相互的, 玩家A 可以看到 玩家B , 玩家B 同时也会看到 玩家A 。当 玩家 在地图中做一些行为操作的时候,如果想要玩家的行为被别人看到,就需要把玩家的行为广播给地图中的所有玩家。场景中玩家的行为只要有修改就广播给场景中其他所有玩家,这种方式是最优的方式呢

  •    对于一些玩家数量较少的场景,比如组队副本等。玩家会看到地图中所有的玩家和npc,可以称之为全视野场景。这种场景中,单个玩家有操作采取广播给场景中的其他玩家的方式是可行的。
  •    对于一些大型的PVP玩法,同一个场景中有几百甚至上千人参加的时候,假设场景中共有1000人参加。任意一个玩家只要移动就要广播给剩下的999人,当1000个人同时移动的话,服务器需要处理999 * 999(百万级别)条消息,相当于 n*n 。场景中所有人移动一下就产生百万级别的数据,还没有考虑玩家的技能释放等其他的行为操作的同步,这么大数据量容易造成服务器消息的堆积,无法做到及时响应。同时,客户端每一个玩家都会处理其他1000个玩家的实时信息,容易造成客户端的卡顿,如果消息处理不过来也会出现包数据的丢失。同时发往客户端的包的数量太大,还会消耗大量的流量。这种方式肯定不是我们想要的,游戏体验会非常差。
          我们可以发现,在游戏中,玩家始终会出现在屏幕的中央,随着玩家的移动,视野也会随着玩家的移动同时移动,但是始终都是以玩家为中心。玩家所能看到的就是游戏屏幕内以玩家为中心向四周360度扩展的视野信息。那么如果修改玩家的 AOI ,控制在以玩家为中心 屏幕内所有所见对象记为玩家的视野数据;当玩家有行为操作的时候,只需要通知在玩家视野中的其他用户,相对于通知场景中的全部玩家来说可以减少大量没有必要的消息的推送,同时也能节省很多流量。因为距离太远的其他用户是不在玩家当前屏幕显示范围内的,把玩家的行为操作通知给这些玩家是没有什么意义的。
          但是如果该场景中所有玩家都在打一个boss,场景中玩家全部都集中在一起,那么玩家当前屏幕中的视野数据又变成场景中的全部玩家了。同时受客户端性能的限制,客户端有最大的同屏人数上限,因此我们需要限制玩家的视野上限。当场景中玩家全部集中的一起的时候按照优先级(社交关系,敌对关系,距离等)进行筛选,筛选出指定数量的对象算入玩家的视野对象。
         因此,针对大型PVP玩法的活动,当场景中人数非常多的时候,玩家的视野采用限制视野上限,并且以玩家为中心360度指定范围设定为玩家视野的方式。以玩家为中心,向四周360度扩展,可以延伸出以玩家为中心的正方形区域,也可以是以玩家为中心的圆形区域。但是由于地图时正方形的,圆形并不适合进行坐标范围临界点的计算,因此最好的就是按照以玩家为中心向外360度扩展的正方形区域。我们可以发现围出来的刚好是一个以玩家为中心的正方形区域。因此可以想到按照各自划分地图区域,玩家在格子的中心,以玩家为中心的正方形区域刚好可以分成九宫格 3 * 3。并且九宫格的区域需要大于等于游戏屏幕的显示区域,根据具体情况合理设计每个地图格子的长度即可。因此玩家的视野数据可以采用九宫格存储的形式。
    在这里插入图片描述




地图

地图划分格子

       在游戏中,地图大小都是正方形,并且是以 X 轴, Z 轴为水平面的(游戏中都以cm为单位)。把地图按照指定的大小分成若干个格子,如以 7m(需要根据实际的地图大小设置单个格子的长度大小) 为单位划分块。当地图被划分为多个地图块之后,通过玩家的坐标点可以计算出玩家哪个地图块中。
       在 X 轴方向进行划分,X轴地图总长度为256m,假设每个块的大小为7m,那么在X 轴,共有多少个格子呢? 256 / 7 = 36.57142857142857,不满足一个格子的直接按照 1 个格子来计算,即X 轴上共有36 + 1 = 37块格子,Z轴同理。假设每个块的大小为8m,256 / 8 = 32,刚好可以划分为32个块。当地图大小固定的时候,不同大小的块,会得到不用的格子数,有的可以整除,有的不能整除(不能整除的不满足 1 块大小算作 1 块),即向上取整。为了避免进行是否能够整除的条件判断,统一采用加 1,即采用 地图长度 / 每个格子长度 + 1 来计算划分的格子数。因此我们划分的总格子数总是会比场景中的地图要大的假设地图大小是256m * 256m,每个格子的大小是7m * 7m,下面都是按照这个大小进行讨论。
        X 轴的总格子数 X_AREA_NUM_PER_MAP = X轴地图长度 / X轴每个格子长度 + 1
        Z 轴的总格子数 Z_AREA_NUM_PER_MAP = Z轴地图长度 / Z轴每个格子长度 + 1
       整个地图共有的格子数 MAX_DYN_AREA_NUM = X_AREA_NUM_PER_MAP * Z_AREA_NUM_PER_MAP + 1(此处加 1 个格子是为了安全起见)
       游戏中可以采用一维数组的方式进行存储地图中所有的格子。格子数 - 1 = 数组下标,通过下标进而得到数组中存储的对应共享内存ID,从而可以得到对应的视野块对象(1个格子可以视作一个视野块对象)。

       地图的视野块(格子)和数组下标的关系如下: 在这里插入图片描述


后面可能会用到的变量:
X_AREA_NUM_PER_MAP ----------- X 轴方向划分的总格子数
Z_AREA_NUM_PER_MAP ----------- Z 轴方向划分的总格子数
MAX_DYN_AREA_NUM ----------- 整个地图所有的格子数
Actor对象 ------------ 玩家 或者 NPC 等统称Actor对象
地图格子对象 ------------ 视野块对象


坐标点定位玩家所在地图块

如何根据具体的坐标点找到玩家所处于哪个格子中呢?
X轴列方向:第一个格子的范围 [0,7),第二个格子范围 [7,14),第三个格子范围[14,21)…
Z轴行方向:第一个格子的范围 [0,7),第二个格子范围 [7,14),第三个格子范围[14,21)…

已知玩家的坐标pos(8, 100, 15),假设地图大小为256m * 256m,格子的大小为7m * 7m。
可以先按照二维数组下标分析:
在 X 轴,X 所在格子数为 X = 8 / 7 = 1,即在X轴上第2列格子中(如图),X轴数组下标为1
在 Z 轴,Z 所在格子数为 Z = 15 / 7 = 2,即在Z轴上第3层格子中(如图),Z轴数组下标为2
按照二维数组计算下标的方式,玩家所在数组的下标为: Z * X_AREA_NUM_PER_MAP + X = 2 * 37 + 1 = 75
在这里插入图片描述



视野块

       地图上每个格子可以看做一个视野块,通过这个格子可以获取坐标位于该格子里面的所有 Actor 对象。那么每个视野块中的所有 Actor 对象是如何存储起来的?

  1. 可以使用HashSet进行存储,存储在当前视野块中的所有玩家的ActorID。HashSet中元素不会重复,同时可以满足快速查找删除,可以满足我们的需求。
  2. 如果以单向链表的形式存储在视野块中的话,查找删除的复杂度都是O(n),显然不合适。可以发现视野块中存储该视野块中所有 Actor 对象的数据结构需要支持快速查找、删除、插入操作。可以想到,通过采用双向链表的形式实现。
    但是双向链表也会弊端,它是通过获取Actor对象上存储的双向链表节点来知道下一个节点的对象的,如果某个对象不存在了,那么就获取不了下一个节点的值,视野块的双向链表就无法遍历了,只能重置。

双向链表存储视野块中所有Actor对象的具体实现如下:
       在视野块对象中只需要存储链表的头结点 + 链表的总数,头结点即Actor对象的ActorID(可唯一标识Actor 对象),然后在Actor对象中存储双向链表节点(链表上一个Actor对象的ActorID + 链表下一个对选哪个的ActorID)。这样通过每个视野块中的头结点即可遍历属于该视野块中的所有Actor对象。
       双向链表删除操作。这样的话当Actor对象移动到下一个视野块的时候,可以很快的进行链表删除操作,BeforeA <----->ActorA<----->AfterA,若要从视野块中删除ActorA,直接BeforeA<------>After A,即可实现。
       双向链表插入操作。当有新的玩家移动到当前视野块,可以采用尾插法的形式实现链表插入。通过视野块对象即可获取到头结点的HeadActorID,头结点的Actor对象中存储了双向链表节点,可以获取到上一个节点的ActorID,即尾节点的TailActorID。即TailActorID<------>HeadActorID,假设要插入的的为ActorA,,则TailActorID<------>ActorA<------>HeadActorID即可实现尾部插入。


假设双向链表数据结构如下:

struct STGIDListNode
{
   
    // 同一链表前一个gid
    ActorID m_iPrevGID;
    
    // 同一链表后一个gid
    ActorID m_iNextGID;
};

双向链表插入节点
stPushNode:要插入的Actor对象的双向链表节点对象
iPushGID:要插入的Actor对象的ActorID
stTailNode:尾结点
stHeadNode:头结点

image


双向链表遍历

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值