游戏开发

IncServer的场景管理及AOI实现

文章目录
  1. 基本概念
  2. Scene
  3. World
  4. Plane
  5. 小结
  6. 场景划分
  7. Land
  8. Patch
  9. 目的
  10. 对象管理
  11. 全局管理器
  12. 分块管理
  13. AOI
  14. 主要操作
  15. 新增对象
  16. 修改对象
  17. 移动
  18. 跳转
  19. 删除对象
  20. 查找对象
  21. 增加管理维度

本文还未完成!

本文还未完成!

本文还未完成!

本文谈谈IncServer如何管理游戏场景中的对象(玩家、怪物、机关等)及其AOI实现。

为了简化讨论,本文略去了游戏对象类型和patch粒度两个维度,在文章末尾会做一个简单的补充说明。

基本概念

Scene

Scene即为游戏场景,可以认为每张游戏地图都是一个scene。比如主城场景、公会战场景、每个单人副本或多人副本的场景。

Scene用于记录跟游戏场景相关的静态信息:

  • 游戏地图的尺寸(height、width)
  • 静态的寻路信息(navmesh)
  • 高度图(heightmap)

IncServer中的scene也有对象管理的功能,但我所接触的用IncServer开发的几款游戏中并未使用这个功能。猜测是在多world的scene中,用于管理在每个world中都相同的静态对象。本文对此不做讨论。

World

World也是游戏场景,与scene记录游戏场景的静态信息不同,world记录场景中的动态信息,尤其是各种游戏对象(以下简称对象),比如玩家、怪物、NPC、机关、掉落物,等等。

一个scene可以对应一个world,也可以对应多个world。对于一个scene对应多个world的情况,每个world就可以看做是一个副本了,这确实也是副本概念的一种实现方式。

不过,IncServer虽然支持scene world一对多,但在实际开发中,scene和world都是一对一的。

Plane

Plane,位面,其本质就是一个整数,可用于实现副本的概念,也可以用于实现主城分线的功能(主城分线可以看做是主城的副本)。

每个游戏对象都有一个plane属性,记录其所属的位面。在同一个world中,只有同一plane的游戏对象才可以互相看到、互相影响,不同plane的对象完全感觉不到其他plane的对象的存在。

小结

每个游戏场景对应一个scene,每个scene对应一个world,每个world可以有任意多个plane。

每个scene、world、plane都有其专属的ID,因此一个游戏对象由scene ID、world ID、plane ID三者共同确定其所属的游戏场景及其所属位面。在实际开发中,只用到了一对一的scene和world,因此scene ID和plane ID两个就够了。

场景划分

由于在实际的开发过程中scene和world是一对一的,因此后文不再区分这两个概念,统一称之为场景

Land

IncServer将每个场景看做是由n个land(块)组成,每个land就是一个大小固定的正方形,默认512 * 512。比如一个scene的尺寸为1200 * 700:

则需要6个land来表示:

Patch

Land进一步被划分为若干个Patch(格子)。以patch为单位,land的边长是2的整数次幂。

目的

为什么要将场景划分成这么多格子?

试想一个场景中有1000个玩家,如果任意一个玩家的状态发生变化,都需要广播给其他玩家,将会是$O(n^2)$的复杂度。而对某个玩家来说,通常情况下只对其附近某个范围之内的其他玩家的状态感兴趣。因此有必要采取一种方式,界定这个感兴趣的范围,即AOI(Area of Interest)的概念。

对象管理

全局管理器

World中的所有对象被放在一个数组中统一管理。

众所周知,数组的随机删除操作是低效的,而有些游戏场景经常会出现游戏对象的新增和删除。比如主城场景,玩家进进出出是再自然不过的事情。貌似链表更适合这种增删频繁的场景,那为什么还要用数组来管理所有对象呢?

设计初衷我不了解,我个人的理解是:使用数组可以方便的将各个对象的处理逻辑均分到多个线程去执行。只要知道对象数组当前所存储的对象数量即可,而用链表就不怎么方便了。

但低效的随机删除问题如何解决呢?IncServer的做法如下:

  • 从world中删除对象时,将其索引存储于一个已删除索引的链表中,对象数组相应的位置置为NULL。
  • 在world中新增对象时,优先取用已删除索引链表中的索引。
  • World中记录对象数组的历史最大索引,即world中对象数量的历史最高值-1。
  • 已删除索引链表中无索引可用时,使用历史最大索引+1作为新索引。
  • 历史最大索引可用于均分对象到各处理线程。

比如,world中原本一共有5个对象,此时最大有效索引为4:

Player 1,Player 4依次离开,索引链表中新增两个已删除索引,最大索引依然为4:

Player 5进入world,从索引链表中取用索引4:

Player 6进入world,从索引链表中取用索引1:

Player 7进入world,索引链表无索引可用,历史最大索引递增1,作为Player 7的索引:

可以看出,对象数组中记录的对象并非都是有效的对象,这不是什么大问题,在使用之前判断一下是否为NULL就行了。

分块管理

World对游戏对象的管理以patch为单位,每个patch都对应有一个对象链表

整个world中所有的对象链表的链表头可依次存储于一个一维数组中,这样根据对象的世界坐标,即可方便的计算出其所属的patch以及对应的对象链表。

假设对象的坐标为(x, z),patch的边长为patch_size,land的边长为land_size,整个场景在x方向上有land_x个land,则对象所属的对象链表的索引计算方式为:

index = (z / patch_size) * (land_x * (land_size / patch_size)) + (x / patch_size);

AOI

与常见的九宫格不同,IncServer支持NEAR、MIDDLE、FAR、FULL四种视野范围:

  • NEAR:当前对象所位于的patch
  • MIDDLE:与NEAR相邻的patch
  • FAR:与MID相邻的patch(NEAR除外)
  • FULL:NEAR + MIDDILE + FAR

不过在实际开发中,所有对象的视野都是FULL,即每个对象都能看到以自己所在patch为中心前后左右各两个patch(总计25个patch)内的其他对象。之后的讨论均假设所有游戏对象的视野范围均为FULL

如果对象B进入对象A的视野范围,则根据A对象类型,可能会触发不同的逻辑,比如:

  • 对象A是玩家:将B对象序列化到对象A的客户端。
  • 对象B是怪物:执行对象进入怪物视野的逻辑,比如激活AI等。

后文将以上情况称之为在对象A的视野中加载对象B,简称为视野加载

主要操作

讨论完IncServer如何划分场景、如何管理场景中的对象,以及AOI,接下来看看场景中的主要操作。

新增对象

在world中新增一个游戏对象需要执行以下步骤:

  • 将对象指针加入到全局对象数组中。
  • 找到对象所属的patch,将对象指针加到patch的对象链表中。
  • 遍历对象视野范围内的所有patch,与各个patch对象链表中的对象互相加载视野。

修改对象

移动

跳转

删除对象

删除对象时新增对象的逆操作,因此反向执行新增对象的步骤即可:

  • 遍历对象视野范围内的所有patch,与各个patch对象链表中的对象互相加载视野。

查找对象

增加管理维度

向world中新增一个对象

  • 先加到等待添加的对象数组add_ary_中()add_obj,等到下一帧统一添加add_ary_中的所有对象
  • 真正添加
  • 添加到链表obj_link_
  • 添加到obj_ary_
  • 改变在obj_link_中的位置
  • 处理视野加载

_modifylink

处理对象的位置改变导致其所在的patch改变,进而引发的视野变化

订阅评论
提醒
guest

0 评论
最旧
最新 最多投票