« August 2006 | Main | October 2006 »

September 24, 2006

《游戏之旅——我的编程感悟》2006金秋读书季

就我自己去年写的那本书,经过了众多朋友的批评,我自己心里面已经认识到了太多的不足,本来不太打算再拿上台面了。昨天收到博文视点出版社朋友的一封 email ,同一天又收到一个读者的来信,让我觉得书本身还是能帮助一些人的。那么,不喜欢这本书的人大可不必读下去,觉得有点用的朋友若有兴趣可以参加这个活动:


您是否读过云风的《游戏之旅——我的编程感悟》?

这本书是否对您的学习和工作有过一些帮助?

您有哪些阅读后的感想希望与其他朋友一起分享?

您还阅读过哪些博文视点出版的图书

请您一起来到这里,参加由电子工业出版社博文视点公司特意为云风举办的“《游戏之旅——我的编程感悟》2006金秋读书季”活动吧!只要您将自己对这本书的点滴阅读感受通过以下方式告诉我们,您就有机会获得精美礼品一份,而且能够同时参加“博文视点3周年热心读者征集活动”,有更多惊喜等着您哦!

获奖名单将于2006年12月1日在云风的blog和博文视点网站上公布。

活动时间:

即日起至2006年11月18日

活动方式:

1) 直接回复本文,讲述您对《游戏之旅——我的编程感悟》一书的阅读感受。

2) 将您对《游戏之旅——我的编程感悟》一书的阅读感受发送电子邮件至 market@broadview.com.cn 参加活动(请在邮件标题上注明“《游戏之旅——我的编程感悟》2006金秋读书季”字样)。

奖项设置:

惊喜大奖(1名):赠送价值500元的博文视点图书(备选书单);

一等奖(5名):赠送第二书店 D币500个(如何使用D币?);

二等奖(10名):赠送设计心理学经典著作——《情感化设计》和启发程序员思考的另类计算机图书——《编程之道》各一册;

三等奖(50名):博文视点精美水杯一个。

图书详细介绍:

http://www.dearbook.com.cn/book/78689

http://www.china-pub.com/computers/common/info.asp?id=27924

http://www.huachu.com.cn/itbook/itbookinfo.asp?lbbh=BI-01026093

更多博文视点三周年活动请访问

http://www.broadview.com.cn/Html/3zhounian/BV3zhounian.htm

“我最喜爱的博文视点图书”票选活动请访问

http://www.dearbook.com.cn/subject/2006pingxuan/index.aspx

本次活动最终解释权归电子工业出版社北京博文视点资讯有限公司所有。

明天去旅游了

加了一周的班,终于告一段落了。

明天去旅游,断绝外界联系一段时间,莫要找我 :)

这段时间关闭留言认证,广告也不会有人来删除了。

September 21, 2006

lua cclosure 的 upvalue 数量限制

最近写的代码中出了一个奇怪的 bug ,很难调试出来。经过一个晚上的挣扎,终于发现了问题。

第一个问题,在 C 函数中,不能随意的时候 lua_State 中的虚拟机堆栈,如果需要大量使用堆栈,应该先调用 lua_checkstack 。少量使用堆栈,(在 LUA_MINSTACK 20 )之下时则没有问题。这个问题其实在文档里有写,我看过忘记了 :( 不过我个人还是觉得 lua_checkstack 的语义有点奇怪,从字面上看,这个 api 不应该有副作用。它能增加可用堆栈的大小违背了 checkstack 的词义。

第二个问题,当从 lua 调用 C 函数时,当参数数量不足的时候,并不会填入 nil 作为缺省参数。比如,写了一个 C 函数,接受两个参数。当 lua 中调用这个 C 函数时,如果仅传入一个参数,那么在 C 中 stack 上 index 2 位置的值并不一定是 nil 。这个时候我们应该用 lua_gettop 得到准确的参数个数以做适当的处理,或者直接在进入 C 函数时调用一次 lua_settop(L,2) 强制堆栈扩展到两个。

第三个问题,就是一开始最为迷惑我的问题。在生成 cclosure 的时候,upvalue 不能超过 255 个。而这一点并没在文档中说明,运行时压入超过 255 个 upvalue 也不会报错。知道仔细查看源码才发现其中的秘密。

按 lua 的设计,upvalue 的个数应该只受内存大小的限制。但是我的程序在生成一个拥有 258 个 upvalue 的 cclosure 时,upvalue 的个数变成了奇怪的 2 个。我的直觉告诉我,2 == 258%256 。查看源码证实了我的想法,cfunction 的 upvalue 的数量用了一个 byte 记录。究其原因,在于 lua 的 object 用了好几个标记量方便虚拟机运作(比如 gc 用到的 mark 位),当 upvalue 的数量也用一个 byte 的时候,刚好四个 byte 凑齐一个 32bit 字。

为什么我会用到如此之多的 upvalue ,这里就牵扯到最近发现的一个给 lua 写 C 扩展的技巧。

一般,C 对象保存在 lua 中都采用 userdata 的形式。userdata 会参与 gc 的过程,所以不必考虑内存释放的问题。但是,往往 C 对象会对 lua 中其它对象做使用上的引用,而 userdata 本身不具备这个能力。以前我的做法是,使用 lua_ref 得到一个整数引用,放在 userdata 中,到 userdata 的 __gc 被触发或者用户主动释放时,作 lua_unref 操作。这样做不太美观,也有一点点效率问题。

最近发现,简单的 C 扩展,我们完全可以用 C closure 实现,把扩展数据放在 upvalue 中。引用的 lua 对象可以直接放进 upvalue ,而其他 C 数据,可以再生成一个 userdata 写入 upvalue 。这个 closure 可以根据调用时的输入参数决定对它内部的数据做何种操作。

用这个技术,我实现了一个简单的循环队列,它会比用 lua table 实现的队列稍微高效一些。使用时,可以调用 C 扩展函数创建一个队列对象(其实是一个 c closure),然后用这个 closure 做进队列和出队列的操作。当传入参数的时候,就把参数进队列,不传参数的时候就把队首的元素出队列。同时可以根据返回值知道队列时候为空和为满。

具体的实现可以看 我的 wiki 上一篇文章

September 16, 2006

心跳服务器

我们目前游戏服务器的初步架构是一个连接服务器处理来自多个客户端的连接数据,这个服务器将所有数据汇总后通过一个 socket 发送到后方的逻辑服务器。这个设计曾经写过一篇 blog 提到过

今天,我在两个服务器之间加入了一个控制心跳的服务器。其原始作用很简单,就是按心跳(目前的设定是 10Hz)从连接服务器上拿到数据,再转发给逻辑服务器。并把逻辑服务器发出的数据转出。

为什么这样做?首先,逻辑服务器不再依赖系统时间。它周期性的从心跳服务器获得数据,获得一次,就认为时间流逝了 0.1 秒。这样,时间就改由心跳服务器控制了。

其次,在心跳服务器上加了一小段代码。可以把从连接服务器上获取的所有需要转发到逻辑服务器上的数据都记录在硬盘的一个文件上。那么,逻辑服务器就随时可以重新请求心跳服务器重复发送上一次的数据流。

显然,这大大方便了调试。因为,逻辑服务器的内部状态改变仅仅只依赖于从心跳服务器获得的输入。它自己的时钟也是由心跳服务器输出的数据的节奏控制,而不被自己的时钟影响。所以逻辑服务器上的伪 timer 也只受输入数据的影响。

这样,即使我们实际让服务器工作几个小时甚至几十小时,在心跳服务器回放数据流时,逻辑服务器可以以无等待的过程全速运行,也可以得到完全相同的结果。当然,如果我们暂停下来单步跟踪,同样不会有丝毫影响。

当逻辑服务器由于某个 bug crash 掉时,就可以反复的重现这个 bug ,直到找到问题了。

心跳服务器的代码非常简洁,区区几百行。其任务也非常简单,所以保证其 7*24 小时稳定运行以及高效率运作是非常容易的事情。至于 client 到达逻辑服务器的路程上又多了一台机器引起的效率问题,我觉得大可不必在意。反正在实际运营情况下,玩家的机器连到服务器的机房,已经路过很多机器了,不在乎加这么一台 :)

September 13, 2006

目前我们的游戏服务器逻辑层设计草案

我们一开始的游戏逻辑层是基于网络包驱动的,也就是将 client 消息定义好结构打包发送出去,然后再 server 解析这些数据包,做相应的处理。

写了一段时间后,觉得这种方案杂乱不利于复杂的项目。跟同事商量以后,改成了非阻塞的 RPC 模式。

首先由处理逻辑的 server 调用 client 的远程方法在 client 创建出只用于显示表现的影子对象;然后 server 对逻辑对象的需要client 做出相应表现的操作,变成调用 client 端影子对象的远程方法来实现。

这使得游戏逻辑编写变的清晰了很多,基本可以无视网络层的存在,和单机游戏的编写一样简单。

本质上,这样一个系统跟网络包驱动的方式没有区别;但是从编码表现形式上要自然很多。正如 C 语言也可以实现面向对象,但却没有 C++ 实现的自然一样。在这个系统中,引擎封装了对象管理的部分,使得逻辑编写的时候不再需要处理讨厌的对象数字 id ;还隐藏了消息发送或广播的问题。

我把玩家控制的角色,和服务器上你的角色分做两个东西。即,你控制的你,和服务器认为的你就分开了。服务器认为的你,你看见的服务器上的其他人是一类东西。操作自己的角色行动时,你通过 client 上的控制器的远程方法向服务器发送指令;而服务器通过远程调用每个角色的远程方法让 client 可以收到感兴趣的所有角色的行为。

这样,client 永远都是通过一个控制器调用其远程方法来告诉服务器"我要干什么",而服务器的逻辑层则通过调用其上所有逻辑对象的远程方法来改变每个对象的状态。而引擎就根据每个链接的需要,广播这些消息,使得每个 client 上对应的影子对象可以收到状态改变的消息。

这些,就是半个月来我跟同事一起做的工作。当然,由于我们用脚本编写逻辑层,这样,脚本接口可以比 C 接口实现的漂亮的多。

首先是自定义格式的接口描述文件,用自编写的工具自动编译成对应脚本代码。我们只需要在脚本中编写对应的类,就可以自动响应远端调用的方法了。而调用远程方法,也跟本地方法保持同样的形式,写起来跟本地函数调用没有区别。这在以前用 C/C++ 编写逻辑的时候是很难做到的。

其次,引擎内部做好对象的管理工作,负责把通讯协议上的 id 转换成逻辑层中的对象传递给逻辑层使用。

再次,enum 这样的类型再也不需要用一些数字的常数了,也不需要在脚本额外的定义出来。可以在接口文件中定义好,经过引擎的处理后,逻辑层可以直接用更为友好的字符串代替,而不失去效率。

编写逻辑的程序员不再需要关心网络的问题后,就可以把心思放在细节上。

最后,对于实现行为预测来补偿网络延迟的特性上。在先前的版本中,我们为了实现这个,花了不少的气力。主要是将时间戳信息放在基础通讯协议中来辅助实现。具体的消息包收到后,再计算延迟时间来推算当前的状态。现在,可以把时间信息封装到 RPC 中,让每个远程方法自动带有延迟时间,方便计算。按模拟程序的实际效果上看,单单位置同步的预测策略,可以让延迟在 8 秒之内的玩家可以忍受;而延迟小于 1 秒的时候,几乎不会受到滞后的影响了。

关于每个链接感兴趣的信息的问题,决定了每个逻辑对象的状态改变要通知哪些人。目前的想法是独立到单独进程去处理,我们在处理连接的服务器和处理逻辑的服务器之间设置单独的服务器来管理每个链接感兴趣的对象,这个任务相对单一且责任重大,独立出来可以大大减轻逻辑服务器的复杂度。

September 08, 2006

读完了《代码大全》

终于把这本书完整的读完了。这本书的后一半远没有前一半读的快。

倒不是说,后面的内容艰涩难理解,或者枯燥无味。仅仅只是因为工作太忙,只能抽出近半个月的时间,每天睡觉前读上几页。也正是这个原因,以前在 wiki 上写的 读书笔记 只有前半本的记录。

诚如书中最后所说,许多程序员在多年工作后,居然一年都不能阅读一本完整的技术图书。我几乎犯了这样的错误。幸运的是,这样一本厚达九百页的好书,最终还是被我耐心的读完了。除了中间几个章节觉得讲的道理过于简单之外,剩下的部分都十分精彩。每每读到一些平时在工作中被讨论过的问题,都让我拍案叫好,我从来没想过这样细节琐碎的东西也会被写到书里。

作者的几乎每一行文字都让我产生了共鸣。从书中,虽然我没有学到新的知识,但是却让我细致的整理了自己的经验。还有一些我原本自己知道正确,却讲不清道理的东西,得以明确的认识。比如关于程序注释,代码风格,不同的软件项目的开发流程等等。正如作者在书中所写一样,我认同他的观点:某些教材中阐述的观点已经被开发实践证明是错误的;而作者如此大胆的以出版文字的形式驳斥那些观点,没有十足的底气是做不到的。

感谢让我们能拥有这样一本好书的所有人。