« July 2011 | Main | September 2011 »

August 25, 2011

绳梯

鸟巢一线,起步的地方很吓人。将近 2 米的间距,需要人从 7,8 米的深坑上空扑过去。虽然不太危险,但需要爬的人克服一点心理压力。我们觉得做一个绳梯把旁边的大石头和岩壁连起来。

自从上次锯掉树桩后,我们又花了一天时间清理线路。还有一些小树,杂草要除掉。工作不比头一天锯树桩轻松。在半空挂了几小时怪累的。

然后就是测量工作。我们决定做四段绳梯,把可笑岩壁下方大仰角都接上梯子。自然岩壁就这点不好,难度不由人控制。这块岩壁本身是很漂亮的。但是下段实在是太难了,基本没有几个人能爬。而上段也就在 5.9 左右。爬得上下段的人,上面索然无味。而上段给初学者或是大菜鸟如我这样的,倒是非常合适。

绳梯是个很好的方案。甚至以后家属来围观也可以玩玩。梯子上可以挂快挂做保护,也很安全。爬上 7,8 米的梯子去到半空,也是难得的体验。

我们花了 300 多块买了 67 斤的尼龙绳,很粗的那种,一捆有 300 多米。高强给出了一种结梯方案,我在 youtube 上找了另一种。在家用帐篷绳子先试了一下。

绳梯模型

最后选了我找的方案。为了慎重,我画了图纸,计算了一番,然后在车库花了一个小时编了一段 2.5M 的梯子。编的时候很小心,最后非常完美,尺寸完全和计划的一致。看起来也很厚实,非常靠谱。

绳梯图纸

编好的绳梯

编好的绳梯

第二天安装后,发现一个小问题。经过踩踏后,梯子的脚踏会收的更紧,原比我们自己用手勒的紧。结果就是每个脚踏间的间隔会变长,比我们预计的长了不少。所以在做第二个绳梯时,注意了这点。重新设计了图纸。

2.5 米 绳梯

绳梯

今天下了点雨,石头很滑。我们很小心的一路做了保护点。最终安装好的效果是这样的。

鸟巢左边的线补了两个挂片,线路完整了。我把它命名为夏天。当然我还没有完成首攀,或许是高强第一个完成,不过他接受了这个名字 :D 如果过两天再开一条的话,我会叫它灰风。懂得人自然明白是什么意思 :)

路绳做好了,等着周末多叫点人上去清理线路。

August 18, 2011

开线流水帐

也算是玩攀岩有一段时间了,一直都是去爬人家开好的运动攀线。玩完就拍拍屁股走人。终于想着也是应该自己做点贡献的时候了。不是说有钱有闲的人是社会发展的源动力么?最近比较闲,请了年假,打算为杭州的攀岩线路做点贡献。强哥在杭州义务开线 n 多条,这次叫上我一起干活了。

2011-8-018.jpg

许久没到可笑岩壁来了。车开到跟前,发现大门封了,门栓都锈死。用榔头敲敲打打才打开。亲爱的可笑 mm 再不爬石头了,可她发现的这块大岩壁还在。凑近一看,变化好大。地面下降了好多,以前开好的线的第一个挂片都跑天上去了。以前清干净的植被又长的郁郁葱葱。按高强的计划,我们要做两个绳梯帮助初学者绕过下面的大仰角,直接从中间起步,享受上段的攀爬乐趣。另外,鸟巢那条线上面有一段枯木桩得锯掉,否则爬到上面的人掉下来得戳了屁股。

2011-8-001.jpg

我们来的早,8 点就到了。在中午 13 点前都晒不到太阳。气温还是很高,拖了上衣,有点风吹过来还以接受。

2011-8-002.jpg

2011-8-003.jpg

2011-8-004.jpg

2011-8-005.jpg

今天的任务是爬上鸟巢一线,清理线路,主要是锯掉枯木以及下面的蔓藤。这条线起步有点心理压力,是在对面的一块 10 米左右的大石头上扑去对面岩壁的。之间有一米多宽。由于下面土地被冲刷,比去年显得更高了。高空作业安全第一,作为保护者,我拴了辅绳在大石头上的树干上。高强有树枝把快挂支过去把第一把挂好,就扑过去上了。

难点在第一个转折点,虽然转过去是个俯角,但几乎是个大平板没有好抓的手点。高强第一次冲坠时我都没啥思想准备。幸亏腰上辅绳长度合适,没直接冲到石头下去。经过两次冲坠后,终于过了难点登顶。

2011-8-006.jpg

2011-8-007.jpg

2011-8-008.jpg

做好保护站,把工具和水吊上去,我就开始爬了。果然起步要克服心理障碍。难点我也掉了一次,后来找到方法也就上了。昨天晚上睡眠时间太短,腿有点虚。我登顶后下来和高强一起停在中段枯树桩处。做了比较繁杂的保护工作,把身体牢牢的栓在半空,换了凉鞋开始干活了。

2011-8-009.jpg

2011-8-012.jpg

我们的工具只有小小的一小锯子,对比直径 20~30 cm 的树桩,我看着都郁闷。高空作业挺难受,小锯子一点点磨。开始我们想偷懒,企图用两个人的体重给掰下来,试了几次都纹丝不动,只好干苦力了。

来来回回的换人,移动保护点移位置,拉了 2 个多小时,累了个半死。浑身汗如雨下。带上来的水也喝光了,我都觉得要崩溃了。再这么下去还搞不定,太阳一翻过岩壁我们就必须留下半拉子工程撤退了。

2011-8-017.jpg

最后的一分钟比想象的来的快。锯了大约 1/3 强以后,靠体重去掰明显有效果了。高强一时兴奋,没来得及让我掏手机出来拍视频。最后一下果然很刺激,啪的一声就断了,然后连人带树干就掉了下去。不过我们的保护点做的很安全,再从半空爬上来就好了。我连连可惜没有把这个过程录下来。

干到 12 点半,我们就收工回家了。结果今天只干了锯树桩这一件事。明天休息一天,后来再接着来干活吧。

August 10, 2011

MMORPG 中场景服务的抽象

MMORPG 中,场景信息同步是很基础而必不可少的服务。这部分很值得抽象出来,专门做成一个通用的服务程序。

此服务无非提供的是,向有需求的对象,同步场景中每个实体的状态信息。那么,我们分解需求,可以看到两点,一是提交状态,二是同步状态。

每条状态信息其实是由三部分构成,状态对象名(key)、状态值(value)、时间。

玩家、NPC、场景中的可变物品,其实都有可改变的状态。比如对象的位置坐标是最常见的状态。其它的状态还有玩家或 NPC 做的动作,玩家离线,上线,等等。

可以有若干数据源向这个服务提供数据,如果借用 zeromq 中的模式的话,这个服务应该使用一个 PULL socket 收集数据。它获取从不同数据源 PUSH 来的,key-value 。然后打上时间戳,储存在内存中。

这个服务另外提供一个发布服务,向所有订阅者广播其收到的状态改变信息,每条信息包括推送来的 key-value 以及附加上去的时间戳。

第三,这个服务应该提供一个请求应答服务,除了订阅模式外,允许别人索取从指定的 timeline 到现在的所有状态。这多用于新的订阅者上线,它需要获取历史上的状态信息。

综上,这三条看起来就是一个 twitter 的基础服务,但缺少了对象之间的关注关系。嗯,在这个简化系统中,这点是可以省略的。另外,状态并不是 tweet 。我们在意的是对状态的修改,而不是同一 key 下的历史上所有状态值。比如,我们只关心一个玩家当前最新的位置,而不关心他历史上在哪里。


这个玩意能做什么?除了同步玩家和 NPC 的位置外,还可以作为战斗系统的基础。比如,玩家做一个攻击动作,可以做为一个状态提交。对于显示来说,场景中的其它玩家都应该看到他做这个动作。但一个额外的战斗模块也可以通过订阅场景,了解到这个玩家做了攻击动作。

之后,战斗模块通过其它途径获得这个攻击动作的具体含义(会产生多少伤害等等),进行运行,得到结论。而进入战斗状态的玩家,可以通过订阅战斗模块发布的信息来获得细节。

嗯,这只是举例说明其一可以想到的用途而已。如果我的归纳没有错误的话,提供上面基本服务的设施,可以做的事情很多,却只需要实现定义好的简单需求了。再列一下:

  1. 一个 PULL socket,获得格式为 key-value 的状态信息。收到后加上 time 储存。

  2. 一个 PUB socket ,一旦受到 1 里面获得的状态信息,即可连同 time 发布出去。

  3. 一个 REP socket ,收到 REQ 请求(一个 time 值),把从这个时间点之后的所有状态信息反馈回去。


下一步,我们看看怎么增强这个服务,解决更复杂的问题:场景很大怎么办?

我们的状态信息还需要增加一个部分,位置。即 key-value-position 。

我们可以增加发布点,把场景分成若干区域,每个区域树立一个灯塔。position 本身会被匹配到临近的几个灯塔上。每个灯塔都是一个子发布点。

增加这样一个服务 :提供一个 REP socket ,收到 REQ 请求(一个position 值)后,返回若干临近的灯塔 id 以及灯塔的信号半径 。然后,用户可以选择订阅这些灯塔。当发现自己的视野即将超出灯塔覆盖后,可以重新索取更换一批灯塔。


关于状态的删除。当物品消失,玩家下线,npc 死亡 等等事件发生后,相关状态,变得只有时间线最后一条信息有意义了。这个时候我建议提交 k/v 对的时候将 v 设置为空,表示把这条状态删除。这样以后用户在请求时就不用获取到无用信息了。

August 09, 2011

Lua 下实现抢占式多线程

Lua 5.2 的开发进度可以回溯到 2010 年 1 月。漫长的流程到今天已经快两年过去,终于等到了 beta 版。我十分期待它可以在 2011 年内正式发布。在这几经折腾的两年里,许多新特性企图挤进 5.2 版,又最终被否决。

当我们审视改进列表,似乎看不到太多耳目一新的东西。但如果仔细阅读一下源代码,就会发现,大部分地方都重新实现过了,以配合这些表面上看起来不大的修改。如果你对 Lua 有足够理解,会发现,这次最激动人心的改进是 "yieldable pcall and metamethods" 。官方也把之列为 Main changes 第一条。语言上的重大新特性 goto 却被列在末尾。

当然,这只是我粗浅的理解而已。没有经过实践使用 5.2 一段时间,下这样的论断有点太草率。不过我还是想谈谈,这点改进可以给我们的开发带来什么。

coroutine 的 yield 现在几乎可以在任何地方使用了。我用了几乎,是因为它依然有一些限制。这些限制不大容易说的很清楚,为了理解其限制,我花了一整天实现阅读 lua 5.2 beta 版的源代码。这个话题下次有机会我再另写一篇 blog 总结一下。今天只谈应用。

我们知道 coroutine 可以实现一个协同多线程模型。即,每个线程(coroutine) 只在用户期望的地方跳出来,并可以在以后跳回去(保持当初跳离的状态)。这解决了许多抢占式多线程的麻烦。lua 的发明人在一篇访谈中谈到了 coroutine 解决并发的问题。但是,让用户不厌其烦的写 yield 是件很讨厌的事情。往往框架会把 yield 调用藏起来。如果我没记错,我读过的早期的 kepler 框架就是把 yield 藏在 html 的输出里的。能够想到的更漂亮的做法是编写一个 lua debug hook ,在 hook 里调用 yield 。这样就可以让 lua vm 每跑几行 lua byte code 就自动做 yield 一次。

可惜的是,lua 5.1 以前的版本不支持这个。因为 yield 限制太多了。如果恰巧 yield 发生在一次 metamethod 调用内部,或 pcall 内部,就会失败。这和 C/Lua 函数嵌套有关。lua vm 在实现 pcall 及 metamethod 机制时,不断的在 C 函数以及 lua 函数间跳转。一旦 yield 发生在多层 C 函数与 lua 函数嵌套调用的内部。用 longjmp 返回的 yield 机制,会丢失 C 函数调用的 stack frame 。也就是说,lua vm 本身的状态可以保留在 L 中,但 C 函数的状态却丢失了,无法正确返回。Lua 5.2 为了解决这个问题,引入了新的 api ,有兴趣可以阅读新的文档的 4.7 – Handling Yields in C 。不过真的想全部搞清楚,还是推荐阅读一下源代码。

只使用系统自带的 debug 库是不够的。用 lua 的 debug.sethook 设置一个 lua 函数做 hook ,里面依然不能调用 coroutine.yield 。这还是受到了 lua 实现的限制。正确的方法是直接使用 C api lua_sethook,把这样一个 hook 函数设进去:

static void
hook(lua_State *L, lua_Debug *ar)
{
    lua_yield(L,0);
}

至于你想让它每隔几行 lua 代码 yield 一次,还是让它发生在函数调用的时候,这看你的喜好。但一旦设置成功,一段 lua 代码就可以透明的定期 yield 出来了。

这些有什么用?我设想了两种用法,

一、是用 lua 做一个调度器,模拟出一个抢占式的多线程库。本质上,这个库是基于 coroutine 实现的。但切换线程是利用的 debug hook 定期强制切换。基于这个库,可以把以前我写过的这个小玩意改进一下。做的更易用一些。

二、是在一个 os 进程内启动多个独立的 lua state ,每个 lua state 并不包含多个 coroutine ,而只用一个 main thread 。设置 debug hook ,让 main thread 可以每跑一个阶段就 yield 出来,把控制权交还到上层的 C 代码。在 C 层次写一个有效的调度器。对 lua state 来说,每个都是独立的,它们之间可以通过 zeromq 这样的库通讯。lua state 还可以部署到多个 os thread 上,实现一个 M:N 的线程模型。它的调度器会比 os 来的更为高效。(关于在语言级实现 M:N 线程模型和 OS 级实现 M:N 线程序模型的性能差异,上个月 7 月 12 日我们在 google+ 上做过一次讨论。可惜是在有限圈子里做的,暂时没找到方法公开)且少占用大量的堆栈资源。

话说到这里,再看看,其实这不就是 Erlang 的模型么?:)

ps. 其实 lua 的早期版本也可以通过 lua coco 实现无限制的 yield 操作。但 coco 使用了 OS 的 fiber 库 这比 5.2 版的 lua 实现多出了额外的堆栈开销。


一些还没实现的想法:游戏服务器里的 npc ai 等东西是不是可以放在一个个独立的 lua state 中完成?它们相互不影响,用消息交互。可以一开始分配好一个 state 池,用来动态绑定新的 AI 单位(减少启动初始化的代价)。把任务切分成独立的小单位,在独立的 lua state 中做,我想对 lua 的 gc 操作也是极为有利的。