« April 2012 | Main | June 2012 »

May 26, 2012

杂记

最近挺忙。我们的计划日期提前了。准确的说是预留了后面处理各种问题的时间。打算在儿童节出一个整合过的版本。

昨天原本是预演一个整合版本的,可大家忙活到下午吃饭时都还有各种问题。晚上 19:50 MIB3 的票都买好了。可是到 19:30 才整合完演示。然后……

很自然的一堆的问题。错过了电影的前半个小时,正好在一个尿点坐到位置上。电影还不错,看的很欢乐。

至于第一历程杯的版本,好在第一期基本功能都差不多了。今天早上起来玩了一上午 Kingdom Rush ,到办公室一看,居然大部分人都不约而同的坐在那了。看来周五那个失败的版本很有紧迫感,最后几天的抓紧时间改 bug 了,这个周末的时间也得用上。我们已经预备 30 号 31 号两天通宵了。


今天下午下了点雨就出太阳了。明天在华工有学生比赛,定了不少新线路,我就去尝了下鲜。可惜今天状态不太好。男子专业组的线路后面太难了,完全过不了,分段试了一下,勉强可以。打算下个月慢慢磨。男子业余组的线路有点意思,虽然没有到顶,但感觉在我的能力范围内。就是在下面没有看好线路,在屋檐下的难点处看漏了一个脚点,所以爬的很憋屈。过了屋檐就脱落了。多试两次应该没有问题。

今天人很多,小朋友特别多,光做保护去了。

最近半年,我的攀岩技术进步很明显。本来以为在我这个年龄,以我糟糕的身体素质,很难有突破了。但的确是贵在坚持,现在一周练上 3 到 4 次,虽然每次都觉得和上次变化不大,可积累下来和去年年底对比,已经有了相当不错的进步。

目前可以顶绳过 5.10c 的线路,5.10d 的也可以尝试一下。先锋攀登也不那么紧张了。5.10a 的线路可以顺利红点。

体能进步尤其明显。现在爬上三四小时,还能跑几圈,踢踢球什么的。估计下次再去天柱岩 就不会在出现被人架着下山的悲剧了。


这半年的新工作感觉专心很重要。虽然以前也有体会,但最近这些年操心的事情太多,很难专心做一个点。创业有几个分工不同的合作伙伴比一个人单干要轻松很多。生活也比以前更有节奏一些。除了攀岩打游戏,平时还能翻翻以前没时间看的闲书了。

心情大好。

May 15, 2012

开发笔记(19) : 怪物行走控制

这段时间项目进展还算顺利,叮当同学在盯项目进度,我专心解决程序上的各种小问题。

最近我在协助解决 NPC (包括地图上的怪物)的行为控制以及 AI 的问题。

目前,我们的进度还处在玩家可以通过客户端登陆到服务器,可以在场景上漫游,以及做一些简单的战斗和技能动作的阶段。按最初的设计原则,我们的每个玩家是在服务器上有一个独立的 agent 服务的。目前写到现在,和中间想过的一些实现方法有些差异,但大体上还是按这个思路进行的。 关键是在于去除大部分的回调方式的异步调用;编写的控制流程自然完整,不需要太多的去考虑 agent 行为之外的交互性。

如果丝毫不考虑性能问题,我很想把每单个怪物和 NPC 都放在独立服务中。但是估算后,觉得不太现实。在 这一篇 的最后一段已经提过这个问题了。

今天展开来谈谈我的方案。

我想把怪物的移动行为独立出来做,以减少 AI 的压力。也就是说,地图上所有的怪,在设定的时候,都可以设定他们的巡逻路径,或是仅仅站立不动。我希望在没有外力干扰的时候,处理这些行为对系统压力最小。

我不想让怪在没有任何玩家看见的时候就让它静止不动,因为这样可能会增加实现的复杂性,并在怪物行为较为复杂时,无法贯彻策划的意图。

最好的方法还是把之隔离,使其对系统的负荷受控。同时也可以通过分离,减小实现的复杂性。

这个子系统是这样的:

它可以接收请求,在单张地图上创建出怪物来。它只关心怪物的坐标。通过 ShareDB 和别的服务分享怪物的坐标。因为它只关心和修改怪物的坐标字段,所以适用于任何结构不同的怪物。

在创建出怪物对象后,可以接受指令,给怪物附加上一个行为。这个行为目前可以是静止,巡逻,跟随别的对象。

这个子系统以一个较低的频率(比如一秒一次),按行为去重新计算所有对象的位置。更新位置后,通过 组播服务 通知地图上所有的玩家 agent 。


这个子系统如何和主系统协作呢?

这里所谓主系统就是地图服务器。我们会把怪物的 AI 模块加载进去运行。怪物 AI 模块会由玩家的 agent 去被动驱动。就是说,如果没有 agent 触发 AI 的处理流程,AI 是死的,不占用 CPU 时间的。每个怪物在生成时,同时通知上述子系统构建一个对象,使它可以在地图中游荡。

怪物由子系统驱动游荡时,通过 AOI 子系统,有可能引发相关的处理逻辑。(一般是进入 agent 的视野,进而触发怪物的 AI 处理)

当怪物 AI 模块接管怪物的控制后,通知子系统删除对应对象,然后进入 AI 控制环节。


这个子系统可以分处于独立进程,独立 CPU 中,并且可以调节控制频率。所以我们可以把这些游荡怪物对系统的负载影响减轻到最小。让系统负荷仅和地图上玩家数量正相关。

对于复杂的怪物逻辑,比如副本中的 BOSS 还是可以以 agent 等价的形式放在一个独立的服务中去处理,这和上面的系统并无矛盾。

May 04, 2012

开发笔记(18) : 读写锁与线程安全

最近一段时间,我正解决的技术问题都是和多线程以及并发有关。

我期望在产品上线后,可以用的上至少 32 核的机器。这样,让多核平摊计算负荷就比较重要。单机承载的人数可以不高,但不希望玩家卡在一起特定的操作上。

这里强调的是单机上的并发,而把跑在不同机器的进程会分开考虑。所以,尽量利用共享内存以及单机锁来解决信息交换和状态同步问题。

上个月花了很多时间解决在 Lua 中并发读取同一份配置数据 的问题。一开始是想用一个 Lua State 储存一份结构化数据,然后让其它的 State 对他进行并发的读操作。我以为这种并发读的行为是线程安全的,但是我错了。因为读 Lua State 是需要对 Lua Stack 做修改,所以在没有锁的情况并不能做到线程安全。

随之我实现了一个改进方案。一开始就初始化好若干 Lua Thread ,在不同的 OS Thread 中使用独立的 Stack 空间操作。因为我们的框架只会启动有限的 OS 线程来跑更多的 Lua State ,这样做是可行且线程安全的。不过还是有一点小问题。如果用户向 Lua State 压入并不存在的 key (string 类型) ,修改 Lua 中 string pool 的操作又有线程安全问题了。当然,这个问题还可以用更为复杂的方法规避(比如再给每个线程配置一个独立的 state 做 string 合法性校验)。

我最后放弃了直接用 Lua State 储存这份共享数据,而改用自己实现的一个线程读安全的 hash 表。Lua 仅作为数据解析和加载过程的工具而存在。整套代码我已经开源了


最近一周,实现战斗服务的 Mike 同学又给我提了新需要。

他说,用我之前实现的 sharedb 是可以用来做角色之间的数据交换。但是,如果有一个 buff 的行为会修改角色身上多个属性的值,那么,单单靠这个无法保证线程安全。逻辑上需要多个属性被同时的,原子的,被修改;而不希望在 buff 去修改一组属性的同时,有另外的进程读取这组属性。这样可能造成读到一部分旧版本的属性及另一部分新版本数据。

我们又不想通过 RPC 消息机制来传递这些属性,最终我打算实现一个锁机制,以角色为单位锁定其附属的所有属性,保证读写的原子性。

一开始我把这个问题想的比较复杂。我希望让我们的框架调度器可以了解锁。如果一个写锁被获得时,读锁所在的 Lua Coroutine 可以被挂起。这样一旦写锁被释放,就可以准确的去唤醒相关被挂起的 Coroutine 。这样做涉及实现一个线程安全的队列挂在每个可以被锁的对象上。我想了一整天后,暂时放弃了这个复杂的实现方案。

最终,我决定暂时先实现一个简单的读写自旋锁。不通知框架的调度器,直接在 CPU 一级阻塞。这样对现有代码和接口改动最小。在 Lua 层封装自旋锁的接口时,尽量少允许用户的使用自由。不提供显式的加锁和解锁 api 。而只让传入一小片中间不会被挂起的 Lua 代码运行。

自旋锁依托于 sharedb 工作,让不同的 lua state 间可以获得到同一个角色数据身上的同一把锁,这样就可以方便的同步数据了。

btw, 在读写自旋锁的实现中,gcc 的 Built-in functions for atomic memory access 非常好用。