« February 2008 | Main | April 2008 »

March 28, 2008

X 下的鼠标滚轮消息的处理

今天午休的时候,抽了几分钟解决我们的游戏 client engine 在 X Window 下无法处理鼠标滚轮事件的问题。

可能这年头直接写代码处理 XEvent 的程序员不多,我 google 了一下,没有找到详细的文档。不过我想这不会是大问题,没有资料就直接看 .h 文件好了。

打开 Xlib.h 通读了一遍,发现没有什么 event 结构是跟滚轮有关的。又绕回自己的程序试了一下,原来,X 下没有特别为鼠标滚轮做单独的消息。(Windows 下则有对应的 WM_MOUSEWHEEL

取而代之的是,鼠标的前滚和后滚被映射到了鼠标的第 4 和第 5 个按键的事件上了。(这个其实可以在 X 的配置文件里配置,相关资料很容易 google 到。)那么,支持鼠标滚轮事件的处理,只需要在 ButtonPress 做处理即可。

March 22, 2008

感觉好多了

其实我并没有用 lua 亲手写过什么大规模的项目。超过 5 千行代码的项目几乎都是 C 或是 C++ 写的。这几天算是做了点复杂的玩意了。几经修改和删减,最后接近完工的这个东西统计下来不多不少 3000 行(误差在十位数)。其中用 C 编写了基础模块 900 多行(仅仅是 socket api 的封装和 byte 流的编码解码),剩下的都是用 lua 设计并实现的。

好吧,我承认 2000 多行代码也只是一个小东西。不过用 lua 实现的一个 wiki 系统,sputnik 还不到 2000 行呢。lua 有一种特质,用的久了就容易体会到。它和 python ruby 这些更为流行的动态语言是不同的。曾经,我把选择 lua 的理由,肤浅的停留在了更轻便更高效上,虽然这些也很重要,但抓住语言的特质才是更关键的。

没有一所学校会教授 lua 这种小语言。教学语言多为 C C++ java (或者更古老一些的 fortran lisp ?)。近年来有听说教 python 的,不过我想 lua 很难进入教学领域。所以大多数用 lua 的程序员,基本上都是半路出家,用过别语言。其中以 C/C++ 为多。我想这是因为 lua 不像 python 有太多成熟的库可以直接使用,要用之到实际项目中去,用 C 为它写扩展必不可少。

或许日后,lua 的扩展库会越来越丰富,lua 程序员不再需要亲自操刀写扩展模块。但就现状来说,我认为也不是坏事。毕竟自己实现的东西最熟悉,(包括 lua 本身的 source code ,规模也不算大,值得一读),知道其性能要点,怎么使用最适宜。

lua 的语言相当简洁,数据类型也少到了极致,真是增一分便觉多余,减一点就有缺陷。这样,我们便不用再把语言用的不好归结到语言的设计本身上。深刻理解它就能用起来游刃有余。

几乎每个 lua 程序员独立写出来的代码都风格迥异。因为我们既可以以面向对象的形式来构建系统,又能够把它当函数式语言实用,还可以直接来描述数据(这是 lua 1.0 的最初用途)。虽然这样,会给大规模项目的多人合作造成一些困扰。但我们能面对多大的项目呢?让我们项目的构架师去把项目划成更小的独立组件,然后用 lua 快乐的实现它。让原本用别的语言需要上万行的工作用 lua 在两三千行中解决。

lua 中复杂的对象类型只有 table 和 userdata 两个。

userdata 往往比较单一,只用来处理效率紧要的数据。这些是放在 C 层次中做的,很容易隔离开。(当然,用之不慎也会出许多问题。不过多为新的 lua 程序员缺乏经验,把 lua 简单看成一个 C 代码粘合剂所致。)

真正复杂的只有 table ,它回相互链接,构成复杂的生存依赖关系。而另外跟垃圾收集有关的类似,比如 string 和 closure (以及后来增加的 thread )则不会带来太多的复杂度。

强调数据类型的复杂度,是关系到垃圾收集的过程。我们知道,lua 这类语言中,不存在 C 语言里的指针越界和内存泄露问题。但是,我不相信垃圾收集就是万能药,在不成熟或有疏漏的设计中,问题只是被隐藏起来了。

所以在 C 或 C++ 中,我们关注数据的内存布局,和内存管理;到了 lua 中,我们也应该关心数据何种情况下诞生,何种情况下消亡。怎样减少垃圾收集器的工作压力。后者并不比前者容易,因为前者往往会引起系统崩溃,给开发人员预警;而后者,表象可能仅仅是系统资源占用过大而已。不引起程序崩溃的失误才是最可怕的。

如果,有性能追求的程序员了解到,每次创建一个新的 table ,构造一个新的 closure ,用连接符连接一个新的 string ,都有新的对象诞生。哪怕它只是临时用几个 circle ,也会给事后的垃圾收集模块造成压力。那么,当他在设计上尽量减少这些事情的发生,且尽量维持着代码规模不至于过大。那么,稳定的系统自然而生。这也是我最近半个月用 lua 编程的感受。

语言,看似是软件设计中枝梢末节的部分。但是语言同样深刻的影响着设计师的思维方式。作为程序员,对单一语言过分依赖的时候,就应该引起自己的警觉,该试点新的思路了。


换个角度再写点别的。

最近很忙,而今天有时间写这么大一篇,纯属无奈。手头一个项目,我自己的部分都做完了。剩下的依赖其他同事一起工作,需要做一些通讯协议上的修正。不过今天是周末,我带团队的原则是,不到万不得已,绝不要求别人加班。写完今天最后一行代码前,已经想过了可以提前写的代码,能做的都做了,剩下的只能等周一继续。

把大项目拆分成独立的二进制模块,分配到不同的人来做。是我最为(也是唯一)认可的中型软件项目的合作开发模式。在游戏客户端这边,基本上是独立模块,留出极少的 C 接口;服务器那边则干脆做成一个个独立进程,之间用通讯协议来协同运行。

经历了太多事情,让我对源码级上的合作失去了信心。或许 XP 的结对编程会好一些,不过前几年尝试的经历让我感觉那样太累。还是一个人对着电脑做东西好。当然前提是个人能力要达到一定的程度,并对编程这件事有足够良性的态度,才能保证代码质量。

代码质量如何保证?我的私人药方是,把一切事情简单化,尽量把代码写短(保持每个独立源文件不超过 500 行),删掉和缩减一切不必要的东西。让 bug 无处发生,比小心的回避它们要容易的多。不行就重写,而不要过多调试。不要在一块代码上耽误太多时间,速战速决。如果花了太多时间去实现它,那么一定有设计问题。世界上有许多复杂的事情难以解决,但绝对不包括实现一个有特定需求的模块 :D 。

怎样高效的完成一个软件项目?选择最好的程序员,不要太多,不要觉得小题大作。把他们放在一起,在一两个月内完成它。如果做不到,比如找不到最好的程序员,只要你相信自己就是最好的,那么即使一个人单干也好过雇一大堆平庸的人去做。当然,再好的程序员也不能同时做两件事情。所以,设定好计划,一件事一件事的来。

程序员们总是高估计自己的能力,而低估问题的复杂度。在多个合作的项目中更为突出。一个人两周的工作,即使两个同样优秀的人一起干,也不会把工期缩短到一周(甚至加到 3 个人也做不到)。道理看上去大家都懂,但是事到临头还是会为多人合作的效率低下而懊恼。可惜,许多许多的现实项目,在不至于引发疲倦的工期内,靠一个人是写不完程序的。何况,像我所处的游戏开发行业内,一个项目不仅仅是靠程序实现。我们还是得面对合作开发这个问题。

March 20, 2008

还真是休息不下来

前几天请了三天年假,想放松一下。周一去周边逛了一下,没觉得有多大兴致。到周二就发觉无聊了,摸到公司看了看。

发现上周发现的一个 bug 还没人解决,跟同事商量着把问题解决了,大约消耗了一个多小时的时间。然后又整理了下新近写的东西的文档,把下面的任务布置了一下。当下决定最后一天年假不休了。

提前一天上班感觉很好,写程序满有意思,就是原来觉得很无聊的文档工作也比闲着有聊的多。

btw, 我们这里有位程序员离开了,劝说他离开的。毕竟每个人都有适合的工作,都有不适合的工作。管理团队确是件复杂的工作,比管理程序项目难。也有挺多东西想写写,却又拿不准要不要写。

另外,又收到两位朋友的简历,我暂时还没时间仔细考虑,请多多包涵。这几天很忙,真的很忙。

March 15, 2008

基于 lua 的热更新系统设计要点

很久没写 blog 了,主要是忙。项目排的很紧,一个小项目上线,发现不少问题,所以把多余精力都投进去了。最后人手不够,亲自上场写代码。在不动大体的情况下,最大能力的修改了一些设计,并把能重写的代码重新写过了。

这一周,写了三天半程序(其中通宵了一次)。平均每天写了千多行程序。基本上把原来项目里用的几万行东西替换下来了。重构总是能比老的设计更好,不是么? :) 不过这事不能老干,个人精力再充沛。故而还是找到称心的合作人比较好,也不能啥都自己做啊。程序员的效率差别可不只十倍二十倍的差别这么少。btw, 前段时间通过这里找到的新同事不错,呵呵。如果有缘,还想再找一个能干的。我相信,聪明人在一起做事可以获得更多快乐。

闲话扯到这里,总结下这两天做项目的一点经验。那就是:当我们需要一个 7*24 小时工作的系统时,实现热更新需要注意的要点。当然,这次我用的 lua 做实现,相信别的动态语言也差不多。只是我对 lua 最为熟悉,可以快速开发。

热更新对于游戏这种需求多变,且需要和用户保持常连接的程序是比较重要的。这样你可以在设计人员不断提出新的需求时,可以在不间断对用户的服务的基础上更新系统。当然快速修补 bug 也是个很重要的用途。

做这样的系统,关键是各种服务要拆分开。使用多进程的设计尤为重要。当的的系统中的各个模块以独立进程的子系统形式出现时,我们只需要把子系统隐藏在连接服务器后,不跟玩家直接通讯。大多数子系统都可以轻易设计成可以动态装卸的。需要更新的时候只需要重新启动一下即可。

这里,数据服务分离也是很重要的。绝大部分服务不应持有太多游戏数据,尽量减少重起服务器中内存数据持久化的带来的程序设计复杂度。

另外,我们需要在后台留下一个管理器的服务,可以通过管理程序向运行着的系统发送指令并得到反馈。从前,我经历的项目都是别的同事在做。可能许多人受 mud 影响太重,倾向于让服务器对一些帐号授予特权,让特权用户可以从普通客户端上以发送聊天信息的形式发送控制指令。这次我自己做,没有采用这种形式,而是在系统内部开放一个管理端口,可以连接管理服务上来监控系统的运作。

以上这些,这两年我在 blog 上已写的太多,就不再展开写了。

让我们关注一下 lua 。这次是我第一次实战 lua 真正实现一个可以热更新的系统。虽然核心部分并不多(大多数服务已经分离成独立进程的子系统了),但是也有几千行代码的规模。管理这些代码,包括他们的更新过程,必须有一些关键点要留意。否则一不小心,运行者的系统多自我更新几次,就会在犄角旮旯遗留一些不容易发现的问题。轻则内存泄露,重则功能不稳定。

首先,lua 的 module 机制,require 会阻止你重新加载相同的模块。这个很简单,在你要更新系统的时候,卸载掉自己编写的模块。方法是:把 package.loaded 里对应模块名下设置为 nil (这样可以保证下次 require 重新加载一次)并把全局表中的对应的模块表置 nil (这样可以避免重加载后留下垃圾)

其次,更新时要保护后内存中的非代码数据。这个时候,对 local 变量的使用务必小心。因为 local 变量总会被作为 upvalue 绑定在 closure 里。我们的代码经常会依赖这些 local 变量。在更新后,许多保存数据用的 local 变量会生成新的一份。这很可能丢失重要数据。而因为这个问题回避使用 local 也是不合适的。要知道 local 和 global 变量的性能可不只差上一点半点。

我采用的方法是,把数据记录在专用的全局表下,并用 local 去引用它。初始化这些数据的时候,首先应该检查他们是否被初始化过了。这样来保证数据不被更新过程重置。

如果要在 lua 中模拟出类机制,一般会使用 metatable 来保存类似 C++ 中的虚表。我们务必要关注这些 metatable 有没有被正确更新。在构造类的时候,生成一张临时的表记录成员函数的话,那么在更新系统工作时,就应该清空老的虚表,再其上重新填写新版的成员函数。而不应该另外生成一张。否则会导致旧的对象的方法无法被刷新,并和后来生成的对象行为不一致。

在做这套系统时,我还遇到一个有趣的 bug :有时候,我们想简单改写一个对象的行为,重载它的一个方法。因为原方法是放在 metatable 的 index 表中,那么重载的方法直接生成 closure 置入对象即可。可有时候,我在新实现的方法最后还要调用老的方法。

比如,我想对 a 对象的 OnConnect 的行为做一些扩展,可能就会这样写:

local OnConnect=a.OnConnect function a:OnConnect(...) dosomething(...) self:OnConnect(...) end

这样先把老的 OnConnect 行为记录在 local 变量中, 重载它,并允许调用老的方法。

在没有自我更新的情况下,这样做没有问题。可一旦代码允许自我更新,它就可能引起多次嵌套。而正确的方式,应该在 local OnConnect=a.OnConnect 前加上一句 a.OnConnect=nil 。

实际使用中,每次都这样写比较繁琐也容易漏掉。最后我写了个简单的函数封装这个过程。btw, lua 用多了,越来越感觉它更偏向于函数式语言,而非 C 这样的过程式语言。怪不得很多人说 lua 跟 Scheme 的血缘关系比之跟 C 更近一些呢。

最后,要做热更新,最好是整个程序一起做,而不要只更新局部的函数或协议。这样可以使更新后的系统更稳定。

先胡乱记录这些,等有空了再重新整理一下思路。

March 02, 2008

MMO 的排队系统

这两周那是忙的天昏地暗。都是些琐碎的事情,两个项目的。理理代码,发发邮件,打打电话,改改 bug ,开开会,签签字,写写报告。周末也加了一天班,工作居然是安装一个论坛系统,外加修改 css ,以及修改模板,调整版面。没办法,时间紧,人手少。

btw, 在服务器上装 php 时,因为开始 ports 没有更新,出了好多问题。mysql 一开始忘记装 gbk 的支持,困扰我老半天。鄙视一下公司购买的某著名 php 写的论坛系统,居然默认不是用 utf-8 编码的。

闲话扯到这里,今天想谈一下上月底,四年逢一次的好日子里,我们公司憋了好久的《天下2》终于又一次体验测试的事情。

首先需要承明一点,我个人不在天下组里,人也不在一起工作。具体细节所知寥寥,甚至不比天下2的老玩家知道的更多。完全是以一个局外人的身份做出评论。

不过我毕竟是网易的老员工,对天下组的同事们私交甚厚。

天下程序的总负责人,是在大话西游一项目失败后,从网易其他部门的技术骨干中被亲点过来加入游戏部的,也是我在大话西游二项目中的老搭档。零四年到现在一直在负责天下这个项目的技术部分。技术有相当的功底,做项目谨慎小心,考虑缜密。我一直认为大话西游二一改前代的程序不稳定的面貌,此人功不可没。不过我个人觉得,他技术上过于保守,在尝试不成熟的技术和想法时有点过于谨慎。

天下组的另几个程序,在当年写作《我的编程感悟》时,曾经就一些技术问题做过反复和深入的讨论(他们的名字我都列入了书里的致谢名单)。都是相处很愉快,可以一起交流技术观点的优秀程序员。

还有几个家伙,大学没毕业就到公司来实习,是我带他们的实习项目。甚至招聘期间就是我自己做的面试和笔试。后来也曾在我负责的项目中工作。当年成立新工作室,有过带出来的想法,都因为能力优秀,被天下这个重点项目留了去,未能得逞。至今甚为遗憾。

套完老交情,下面可以开始不客气的批评了 :) 这次新版的天下2 的登陆排队系统(是 Big world 后来新版官方加的吗?没去了解)真是做的太糟糕了。


据说这次“体验测试”,一共发放了 5000 个帐号。当是限制比较严格的,我自己也没有号。倒是我们这里另一个同事是某个游戏大公会的成员,分到一个号,得以第一时间上去看看。我也旁观了这个游戏。

那么,理论上峰值同时登陆进游戏的人数应该不超过 5000 人了吧。离 big world 的理论计算的压力承受能力差的远呢。所以说,理论和实际是有差距的。购买 big world 引擎的开发商很多,但能向网易这样投入大量技术精英参与改进的公司恐怕不多。如果网易都不能把 big world 用好的话,我看别的开发团队也难。

前几个月,John Romero 来拜访我们公司,我特定慕名去上海见了一面。他们也想(正在?)开发 MMO ,席间问及 big world 的情况,我毫不客气的指出 big world 诸多缺点。关键的一点就是 big world 设计之初更偏向于 FPS 的类型,把大量的 cpu 和带宽放在了角色间 3d 空间位置同步上了。这很可能不适合 其它 MMO 类型游戏。我坚信,在目前的硬软件条件下,MMO 用的 engine 一定要恰如其份的适合设计人员构思的游戏类型。想通过购买现成的 engine 来减少开发成本,本意虽然不错,但功用可能有限。

当然,通用 engine 这条路迟早会走下去的。在不远的将来某一天,CPU 和带宽都按照摩尔定律发展下去,一些条件的制约会弱化。正如今天 3d 显卡和软件接口的发展一样,总有 Unreal 3 这样的优秀 3d engine 大行其道。

好象又跑题了,打住打住 :)


说回排队系统。大家都说 MMO 里的排队系统是魔兽世界的首创。可是只要是有社会经验的人,一拍脑袋就能想出这个点子来。谁都知道,在大量人拥挤在一起想完成同一件事情时,排队遵守次序是提高整体效率的最佳方案。那么,为什么 MMO 发展了那么多年,只到 wow 才被用于玩家登陆?

理由只有两个:1. 问题(太多玩家同时登陆)不存在,不需要去做。 2. 做这样一个排队系统,有一定的技术难度,不容易做好。

29 号那天下午,天下的排队系统是启动了的。可是似乎不解决任何问题。反而大量不能进入游戏的玩家在排队系统中拥挤着,甚至导致了系统不稳定,进入游戏的玩家纷纷掉线。

到了 1 号,开发组似乎把排队系统关闭了。游戏服务器达到承载上限的时候,干脆拒绝任何新的连接。这个时候玩家看到的只是服务器没有回应。讽刺的是,这个时候,游戏服务器反而稳定下来,至少已进入游戏的玩家可以正常游戏了。

任何能弄清楚到底需要什么的项目,在工程上都是容易实现的。无非克服技术点而已。(反之,需求模糊的东西,就很难顺利完成。这也是 MMO 游戏业抄袭成风的根源。毕竟抄袭让我们知道该做些什么。)我相信排队系统终究可以完美实现。下面分析一下排队系统的需求,和技术难点。

让服务器组可以在短时间接纳大量的连接,并将暂时不能处理的连接分流。分流的连接必须保持住,而不能随意丢掉,避免这些断开的连接重新(人为)回来,再次造成冲击。

这里的难点其实在于,系统如何做这样的分流,让处理排队业务的部分不成为游戏逻辑处理业务的桎梏。btw, bigworld engine 还有一个难点,由于它设计之初的决定,通讯协议采取了 UDP 而没有使用 TCP ,所以保持连接更加困难。我并没有说使用 UDP 协议通讯有任何不好(对于 big world 的整体设计来说,采用 UDP 是正确的),只是这里的确造成了一定的技术困难。

以我有限的技术眼光,能开的药方还是从最近两年在忙的这摊子事中得到的。那就是:

  1. 身份认证系统分离。参考我以前写的 多服务器的用户身份认证方案 ,身份认证系统可以单独想办法做成集群,处理大量并发请求。但由于每通过一次身份认证只能拿到一次性令牌,这样就在身份认证系统和游戏系统之间做了一个缓冲层。

  2. 采用连接服务器和逻辑分离的设计。并且为一组服务器增加多个连接入口点。可参考:游戏服务器处理多个连接入口的方案 。这样,即使游戏服务器组同时只能处理千级别的玩家,连接服务器组一样可以承受万级别的同时连接数。同时可能方便的留出特殊通道供内部人员使用。据说前两天,天下服务器拥挤的时候,我们自己的 QC 人员都没能登陆进游戏系统。更别说老板想进去看看游戏了 :) (我不赞同内部人员就能比外部玩家有更多的特权,实际上网易的游戏一直也是这么坚持的。但是维护和测试人员在测试期也挤不上游戏服务器就有点搞笑了)

  3. 游戏系统尽量使用多进程流水线设计。参考:多进程的游戏服务器设计 。把排队系统独立到一个进程里,放在连接服务器之后。在负载过高的时候,单独分配到一台物理机器上工作。游戏排队系统逻辑简单,并可以维持较低的通讯/心跳频率,所以单台机器处理几万逻辑连接(真正的海量 TCP 连接是在连接服务器上做的)是没有什么问题的。

这里,处理排队的进程,实际也担负了监控游戏服务器组的负载的任务。所以它可以在正确的时候放行排在前列的玩家。独立的进程使它不至于影响正常游戏的逻辑处理。这样一个子系统,我们的 engine 中被称为 watchdog ,代码不过几百行。这块设计在 去年底的那次演讲 中也讲过了。不知道当时在场的朋友中有没有人关注。 :)

以上提到的诸点,并非在 big world 中全无。只是 engine 开发受制于人,又有些许沟通上的障碍。最终制约了我们自己的技术人员解决问题的粒度和速度。我想,若是欧美厂商购买了 big world ,能够用的更好吧。

现实是复杂的。没做到,没做好,并非我们的程序员不能也。


3 月 3 日补充:

这两天好几个天下组的同事找到我,给我讲这件事情的经过 :) 原来并不是排队系统没有做好。而是别的原因。(包括一些其它 bug ,服务器初期玩家过于拥挤在同一地点等)现在游戏服务器已经很流畅了。