AOI 服务的设计与实现

所谓 AOI ( Area Of Interest ) ,大致有两个用途。


一则是解决 NPC 的 AI 事件触发问题。游戏场景中有众多的 NPC ,比 PC 大致要多一个数量级。NPC 的 AI 触发条件往往是和其它 NPC 或 PC 距离接近。如果没


有 


AOI 模块,每个 NPC 都需要遍历场景中其它对象,判断与之距离。这个检索量是非常巨大的(复杂度 O(N*N) )。一般我们会设计一个 AOI 模块,统一处理,并


优化比较次数,当两个对象距离接近时,以消息的形式通知它们。


二则用于减少向 PC 发送的同步消息数量。把离 PC 较远的物体状态变化的消息过滤掉。PC 身上可以带一个附近对象列表,由 AOI 消息来增减这个列表的内容。


在服务器上,我们一般推荐把 AOI 模块做成一个独立服务 。场景模块通知它改变对象的位置信息。AOI 服务则发送 AOI 消息给场景。


AOI 的传统实现方法大致有三种:


第一,也是最苯的方案。直接定期比较所有对象间的关系,发现能够触发 AOI 事件就发送消息。这种方案实现起来相当简洁,几乎不可能有 bug ,可以用来验证


服务协议的正确性。在场景中对象不对的情况下其实也是不错的一个方案。如果我们独立出来的话,利用一个单独的核,其实可以定期处理相当大的对象数量。


第二,空间切割监视的方法。把场景划分为等大的格子,在每个格子里树立灯塔。在对象进入或退出格子时,维护每个灯塔上的对象列表。对于每个灯塔还是 


O(N * N) 的复杂度,但由于把对象数据量大量降了下来,所以性能要好的多,实现也很容易。缺点是,存储空间不仅和对象数量有关,还和场景大小有关。更浪


费内存。且当场景规模大过对象数量规模时,性能还会下降。因为要遍历整个场景。对大地图不太合适。这里还有一些优化技巧,比如可以把格子划分为六边


 的。

第三,使用十字链表 (3d 空间则再增加一个链表维度) 保存一系列线段,当线段移动时触发 AOI 事件。算法不展开解释,这个用的很多应该搜的到。优点是可以


混用于不同半径的 AOI 区域。


接下来我要来实现这个 AOI 服务了,仔细考虑了一下,我决定简化以前做的设计 。


首先是最重要的协议设计。以前我认为,AOI 服务器应该支持对象的增删,可以在对象进入对方的 AOI 区域以及退出 AOI 区域时发出消息。




这次我重新思考了一下,觉得是可以简化的。


我只打算支持固定 AOI 半径,下面是我重新设计的协议:

 
 
typedef void * (*aoi_Alloc)(void *ud, void * ptr, size_t old_sz, size_t new_sz);

typedef void (aoi_Callback)(void *ud, uint32_t id, uint32_t id);

 
 
struct aoi_space;

 
 
struct aoi_space * aoi_create(aoi_Alloc alloc, void *ud, float radis);

void aoi_release(struct aoi_space *);

 
 
// w(atcher) m(arker) d(rop)

void aoi_update(struct aoi_space * space , uint32_t id, const char * mode , float pos[3]);

void aoi_message(struct aoi_space *space, aoi_Callback cb, void *ud);


核心只有两条协议,即最后两个 C API 。update 用来更新对象的状态;message 用来获得 aoi 消息。


这次不打算发送离开 AOI 区域的消息,而只会发送进入 AOI 区域的消息。如果对象始终维持在 AOI 区域中,尽量不发送新的消息,不确保这一点,但不会有过于


频繁的消息推送。


我把对象分为两类,一类叫观察者 Watcher ,另一类是被观察者 Marker 。一个对象可以同为 Watcher 和 Marker 。在 Marker 进入 Watcher 的 AOI 区域时会触


发消息。


对象用 32 位 id 表示;update 的 mode 参数用来表示这个对象是 Watcher 还是 Marker 。形式如 fopen 的 mode 参数。写上 "wm" 表示即为 Watcher 又是 


Marker 。mode 还可以传入 "d" 表示 Drop 丢弃掉这个对象。


为什么不需要退出 AOI 区域的消息?我认为,在使用 AOI 服务时,往往只是用来简化比较距离的操作。收到 AOI 消息后,用户可以选择把对象加入自己关心的列


表。以后在处理遍历这个列表时,有足够多的机会把不再关心的对象删掉。具体使用时,建议在对象超过两倍 AOI 距离后再取消关注。后面会看到为什么不能在


对象离开 AOI 区域后立刻删除,因为那样可能导致不再收到重新进入 AOI 区域的消息。


使用时,定期调用 message 函数。每次调用称为一个 tick 。在这个 tick 里,会把发生的 AOI 事件以回调函数的形式发出来。


以上 API 很容易封装为一个网络服务,方便使用。接下来几天,我将按下面提到的算法实现这个 C 模块,并且开源。 :)




这次我不打算用打格子的方法来实现 AOI 模块。经过一番思考,我觉得我找到了一种更好的算法。


我把对象的状态分为三类。


第一类称为静止 (static) 。当一个对象在当前 tick 内没有更新坐标,就认为它是静止的。


第二类称为微动 (shift) 。当一个对象更新了坐标,但新坐标和上个关键坐标距离较小(不超过 AOI 半径的一半)时,我们认为这个对象是微动的。


第三类称为运动 (move) 。当一个对象第一次出现,或它的新坐标离上个关键点距离较大时,我们认为这个对象在运动。进入运动状态的对象会把自己的关键点坐


标更新为当前位置。


在 AOI 空间中,我储存了 6 个集合对应 Watcher 和 Marker 的三类状态。一开始,这 6 个集合都是空的。


另外,在空间中还保存有四个集合,属于新增的微动对象 (shift new) 和新增的运动对象 (move new)。


在 update 函数中,新的 id 一定被加入 move new 集合。需要删除 (drop) 的 id 则简单打上 drop 标记。以前提到过的 id ,则查询更新的坐标和旧坐标的距离,


更新其状态。如果有状态较之以前有改变,则分别置入 shift new 或 move new 集合。否则什么也不做。




message 函数被调用时,处理这些集合,就可以获得 AOI 消息。算法如下:


一,遍历 shift 和 move 集合,如果发现里面的对象当前 tick 没有更新,则把它放到对应的 static 集合中去。如果状态发生改变(从 shift 变成了 move 或从 


move 变成了 shift )则从对应集合删掉,因为它们已经存在于 shift new 或 move new 集合中了。如果对象处于 drop 状态,则减掉对象的引用(对象用引用记


数管理)。


二,把 shift new 集合里的对象和 static , shift 以及 shift new 集合里的对象逐个比较。如果他们距离较近(两倍 AOI 半径之内),则生成一个热点对,放到热


点对列表中。(关于“热点对”,下文会展开解释)。这里的比较指 Watcher 集合和 Marker 集合的交叉比较。比较双方都同为 Watcher 和 Marker 时,注意只比


较一次。这很容易实现,因为可以用 id 的大小来做鉴别。


三,将 move new 集合里的对象,和 static , shift , shift new, move , move new 的对象配对一一加入热点列表。不必做距离判断。


四,将 shift new 合并入 shift 集合,move new 合并入 move 集合。




五,在每个 AOI 空间中,都有一个列表,我们称为热点队列表。每个热点对,是我们需要尝试判断是否会触发 AOI 消息的两个 id 对。这个列表里会有哪些 id 对


呢?如果你理解了上面几个步骤的处理,就能想到,包含有至少一个运动对象的对象对,距离比较近的微动对象对;没有完全处于静止状态的对象对,也没有距


离较远的微动对象对。我们在处理这些热点对时,比较它们和上个 tick 处理时,对象状态是否发生了改变。只要至少有一个对象对象发生了改变,就将这个热点


对抛弃。(因为一定有新的正确关联这两个对象的热点对在这个列表中)对于其他有效的热点对,我们比较其中两个对象的距离,当距离小于 AOI 半径时,发送 


AOI 消息,并把自己从列表中删除;否则保留在列表中等待下个 tick 处理。




以上算法略微有点复杂,但实现起来并不困难。为什么它效率很高呢?


因为,如果对象处于某种速度跨越不大的运动状态中,而 AOI 距离和运动速度相比比较长,那么,运动的对象将比较长时间的停留在微动( shift )状态。如果

对象停止了运动,则会切换到静止 ( static ) 状态。这两种状态之间的对象,若距离比较远,他们将不会进入热点对,及不会被遍历,也没有比较距离的运算。


只有处于运动状态的对象,会在每个 tick 和其它所有对象做一次比较。而只有少数高速运动或跳转的对象会被打上 move 标记。一般微动 (shift) 的对象则以一个


周期被标记为 move 。这些操作是低频的。


完全静止的对象之间的遍历和比较则完全被优化掉了。




这篇是一个粗略的想法。大概需要这周余下的时间来实现。在实现过程中,可能会发现一些没有想到的细节,届时再来修改。




3 月 7 日:


昨天花了一天实现。发现许多可以简化和改进的地方。我认为可以把 static 以及 shift 都合并到 shift ,简化处理流程。


在实现集合的时候,一开始先用双向链表。而关系对用 hash 表。实作觉得过于复杂,就简化为数组和单向链表了。每次 message 生成的时候用一个 O(n) 的遍历

生成几个需要的集合。这样比维护双向链表实现的集合的变化要简洁,性能感觉也不会有太大的损失。


代码已经放到了 github 上。大致应该是正确可用的。内存分配器可以定制,如果需要优化性能,定制一个内存分配器很重要。因为运动期动态的内存增删都是一


针对 


pair_list 固定大小的。专门定制可以减少内存占用,减少碎片,加快速度。实现也很简单,做一个固定大小的内存池即可。有空我会实现一个默认的定制内存


分配器。


关于测试,今天小 V 同学帮我写一个一个图形化的测试程序,可以直观的看到效果,并核对有没有 AOI 消息的遗漏。大体上是没有问题的。

aoi.jpg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值