« March 2008 | Main | May 2008 »

April 30, 2008

那些日子(一)

明天就是五一假期了,同事都已放假。我不打算在假期加班,因为加班也无事可做,手头上的工作都需要与人合作。

前几天和新同事吃夜宵,大家聊的异常兴奋,我也忍不住开始想当年。当年那些美好的日子,记忆已经很模糊了。我想再过个两年,估计我都不能准确回忆起那些曾经对我影响深刻的日子准确的时间。是时候记下点什么,对自己是一种纪念。

我这人有个优点,选择性记忆,那些不快的回忆很容易随风而去。活在我记忆中的人们,对他们只留下感激。我也曾经爱写日记,很早我就写电子日记,记在自己的机器上,PDA 上,当我有一些不愿意再回忆的事情时,我会个将整个文件加上密码,长长的一次性密码,保证自己只能记住一小段日子。当这段日子过去,密码就消失在记忆中。然后再也打不开这些文字,等到下次更换硬盘,无论我多么的想再看一眼当年的自己,也无能为力,只好把加密过的文件删去。

我想我就是这么成长过来,没有什么挫折的感觉被反复咀嚼,都已经抛在脑后。生命中没有什么不可以失去的,这个道理很早就明白了。我曾经懊恼过丢失了大量的源代码、自以为写的不错的文章、早年的聊天记录、珍贵的日记、数年的电子邮件…… 最后我明白了,一切的一切不过是身外之物,我能拥有回忆中最美好的部分,那么已经是特别幸福了。

不过也正是如此,以下的记录也只能是我努力的回忆。或许因为时间久远,跟真实有所偏差,或许从我的角度只看到的事物的一面,但是、我可以保证,并没有故意在叙述中掺差虚假的东西。

是的,我想讲一个真实的故事,一个拥有数千万玩家的游戏诞生的故事。我并不喜欢这个游戏系列本身,但是我为这个产品自豪。我的代码曾运行在几千万用户的机器上,作为一个程序员,还有什么比这更让人满足的呢?也许有,比如让这个用户数量再扩大 10 倍。


认识“古越”还是我读大四下学期的时候(2000 年初)。有一天,他在 QQ 上蹦出来,问我一些“风魂”的问题。我当时上网主要在泡 sina 的游戏制作论坛,“风魂”就是那几年写的游戏之作。

更早的时候,我比较喜欢在 dos 下写点东西。研究一下 allegro 这个游戏开发库。我翻译了 allegro 第 3 版的所有文档(为此还自己做了一个辅助翻译工具),这项工程耗去了很长的时间,从 98 年开始到 99 年中,业余时间我几乎都在维护这个东西。为了翻译不出差错,同时也阅读了大部分 allegro 的源代码,从中学习到了许多游戏引擎的理念。

那些日子,时常在 allegro 的 maillist 说几句话,为一些代码做优化并迅速被 allegro 开发社区吸收进去。同时我也提出了许多自己的想法。不过由于新的想法需要对 allegro 的接口做调整,这是一个成熟的库不可接受的,和 allegro 的原作者 Shawn 通 email 的过程中,Shawn 用很友好的语气说,如果你觉得那样比较好,为什么不自己做一套东西出来?然后我就做了,甚至第一个版本在 allegro 的 mallist 中发布。有人说,这样的东西没什么意义,allegro 已经够好了(当时已经有了 Windows 版)。Shawn 还帮我解释。

哦,我说的就是“风魂”。甚至不到一个月,风魂就有了一个匈牙利用户,他还用它做了一个小游戏。

这是 1999 年 3 月 4 日到 3 月 8 日的事情。我在网吧通宵了三个晚上把风魂的第一个版本完成。之所以日子记的这么清楚,是因为我查到了当年留下的一份记录文档。开发环境是 MSVC 5 ,因为我不愿意(也没有足够的硬盘空间)装 IE4 ,所以没有安装 6.0 的 VC 。


“古越”,就是天夏的 client 主程,也担当了后来大话西游1 的 client 主要逻辑的编写工作。那个年代,精通 Windows 写游戏编写的人不多,我也只是稍微熟悉而已。很多人刚从 dos 年代过来不久,DirectX 的中文资料很少,且比较难查到。我很能理解他们选择使用“风魂”这个学生作品的缘故:开源 + 使用简单(简单的 C 接口) + 高效(在硬件条件受限的时候,我在软件优化上下了许多工夫)。

天夏这个小公司当时正在开发一款图形 MUD ,名字就叫“天下”。当时估计有很多 mud 迷想把 mud 图形化,但是做出来的产品寥寥。我只记得有一款叫作“笑傲江湖”的所谓图形 mud ,仅仅只是给文字 mud 加了点图片而已。真正意义上的图形化还没有人完成。

显然,天夏的开发团队也没有前人的经验可以追寻,甚至他们没有开发过单机游戏。忘了当年“古越”问了我一些什么,只是最后,他想请我帮忙做一些模块,可以让游戏开发更简单一点。这个工作是有酬的,这点吸引了我。要知道当时都是穷学生,我连买块硬盘的钱都没有,显示器也已经严重老化(93 年购入的时候已经是国外的电子垃圾,不知道服役过多少年了), 我的开发机器中的 CPU 是网友的公司赞助的,主板是编程比赛的奖品,内存条这些配件用的先前一些兼职工资买的。

所以,任何一个用自己的技能赚钱的机会都不会放过。这样,我又认识了 micro ,天夏当时的头儿之一,据说他当过 mud 的巫师,也写一些服务器的代码。不过后来我们见过面之后,一起在网易共事的日子里,几乎没见他再写过什么代码,这些是后话了。

我写了一个支持 Z buffer 的 2d 模块。这样,他们可以简单的处理 2d 游戏中 sprite 的遮罩问题。因为需要让当时配置比较差的机器(486)能跑起来,我尽可能的用汇编优化。这些工作耗费了我一两周的时间。

快完成的时候,我在网上询问了朋友(逆火的庞鑫,他与我同届,但是他在大学期间已经发行了一个游戏了:天惑),庞鑫告诉我说,他们为了养活自己的工作室,时常也接一些单来做。这样的单大约应该开个 1500 的价。当时我天真的觉得,1500 实在是个天价,要知道 97 年的时候,我帮人用 delphi 1.0 做了一个完整的软件也才拿了 600 多点,那个用了我半个暑假。

所以我跟 micro 提的价码是 1000 。有点意外的是 micro 还是觉得有点高了,不过我理解他们的艰辛(当时是一个很小的工作室,没有什么投资,几个人自己在弄),重新核算了一下,把自己花掉的时间统计了一下。按每小时 20 块(比家教的水平高多了,当时就这么想的)得到一个大约 500 的数字,micro 把款打给了我。这就是我和天夏的第一次合作。

btw, 具体的数字我记不太清了,只能说大约这个数量级吧。大学毕业后我就再也没缺过钱用,对钱的数字极其不敏感,所以忘的快。


毕业的第 2 天,我去了北京。在创意鹰翔待了三天。林广利是我很好的朋友,我看他似大哥一样。他邀请我去的北京。鹰翔当时的情况看起来不是很好,不过我不太所谓。总算毕业了,我觉得我自由了,再也不用看老师的脸色,不用应付烦心的考试,不用担心课堂点名……

当被问及我们应该重新开始做个怎样的游戏时,大家并没有想到网络游戏,虽然那个时候石器时代已经开始流行。我听到的圈子里的说法是,“目前国内有 200 个网络游戏正在开发,明年至少有 20 个上市,再开始做已经晚了,也没啥意思”。事实上,2001 年并没有那么多国产网游被开发出来,我所熟知的一个:大话西游,可耻的失败鸟 :D

那几天我们讨论了许多,但是不知道为啥,我始终没什么兴趣。我不知道我需要什么,但是我知道那些不是我想要的。

逆火工作室在创意鹰翔对面的院子里,我联系庞鑫出来聚一下的时候才发现这一点。太巧了,北京如此之大,此刻又如此之小。

庞鑫是个典型的北京人,说话极富煽动性(会忽悠?不过人家也的确有真本事)。我 98 年在北京参加一个大学生计算机比赛时,认识的他。他倒不像我是参加编程部分的比赛,而是展出他的 3d engine 。98 年,3d 显卡还是很稀罕的年代,所以我很是佩服。

庞鑫试图说服我加入他们的工作室,跟随一个投资者(yan dan ,那个时候听说很有名,他说他帮过许多人,比如雷军)做手机的软件。据说很有前景。EPOC 系统,我第一次听说,现在已经改叫 Symbian OS 了。我们为 EPOC 做一个 3d 游戏引擎,应该很不错的。我可以做我最熟悉的一块,在 arm 处理器上,用 arm 汇编优化 2d 部分(因为手机当时还不能配备 3d 硬件,都是软件实现,最终都需要转到 2d 显示)以及一些底层核心代码。

安宁(后来为天下 2 写了几年的程序)当时也在那里,我挺喜欢这个人,他曾经用汇编写了一个软件 3d engine 。符合我心目中的牛人标准。

庞鑫的说服工作只用了几个小时就成功了。我给林广利打了个电话,他只是叹气而已,没太说什么。我就直接住进了逆火工作室租的那间大屋子。

那段日子过的挺自在,都是一帮好朋友,住在一起,半夜饿了就打车出去永和喝豆浆。白天叫些外卖。伴晚时分,也出去闲逛。我在北京有很多朋友可以一个个蹭饭 :)。几乎都是游戏圈子的。比如,最早拉我进入这个圈子的王欣(八爪鱼工作室的负责人);为无数国产游戏“擦过屁股(修补 bug 并完成游戏好拿出来卖)”的余雪松(可能说起后来的 kele8 大家更熟悉一下)等等。

这段日子一直过到天气开始转凉。我还清晰的记得那一天,有人通知我们第 2 天去深圳见风险投资的公司代表。我们连夜做了 PPT ,赶上第 2 天早上第一班飞机南下。我们只在飞机上打了个盹就被拉到另一个合作的团队的驻扎地。这个团队当时叫做 wass ,后来其主干有建立了家新公司唤作“数位红”,现在好象已经不存在了,不过应该 google 的到。

原来投资商是再后一天从新加坡去广州的。我们在深圳只是再准备一下。这次让我领略了该怎么忽悠风险投资 :)我们掐着秒表准备演讲内容,甚至连中场休息的笑话都是预备好的。

我的部分不太需要准备,反正不是说大话,怎么想怎么说即可。所以得以有两三小时的睡眠。等天再此蒙蒙亮的时候,我们已经上了去广州的 taxi 。几乎是一睁眼就到了,除了困,我什么都不记得了。

我和庞鑫的演讲部分安排在最后,会议室里,我们拼命的喝着不加糖的咖啡,中间上了三次厕所。我和站在外面吸烟处猛着抽烟的新哥们相似而笑。 40 多小时没怎么合眼。大家都困死了。

投资商是宏基,看起来比较重视,派了个元老过来谈的,带了很多人。我和庞鑫的演说很成功,想来是因为我们年轻,表现的相当有激情,甚至还为技术细节吵了两句。说完了我们就回房间睡觉去了。晚上有人来敲门的时候,已经带着好消息。

虽然我们要求的四百万美刀没有被答应,不过对方还是答应投资 1.5M 。我们这大摊子人一共保留多少股份不太记得了。只记得逆火工作室的哥几个分到百分之十几。

我们需要签四年的合同,合同很厚很厚,回北京后还安排了律师给我们讲解。我觉得学到了许多。只是最后签字前的那一晚,我想了一个问题:这是不是我的追求,我要的人生?

庞鑫曾经跟我说,没错,如果我们有 15w RMB 我们就可以自己干。即使大家现在一无所有,只要有技术,无论做点什么,不需要多少时间就可以赚到这么多钱,然后开始做自己更想做的东西。但是,或许有更好的路,让我们就有更高的起点,节约我们的生命。我知道逆火的兄弟曾经过的很辛苦,《天惑》做了三年多,只卖了五万块 RMB ,而且出版商还拖欠款项很久。但是我没有亲身经历,我不喜欢手机这个开发平台,离我的想法太远。

在那个大屋子里,大家围坐在一起,劝我不要走。我也很犹豫。负责投资的那个头儿接了电话过来,只问了我是否真的想离开。我说,我不确定,但是我知道,这些不是我想要的。这句话,很多年后我又说过一次。每次说出来,都得到了理解。就这样,我离开了这个团队。

下次再接着写吧,我想会一直写到梦幻西游的成功,是个很长的故事,不知道有没有朋友会一直读下去。我只是想记录这几年的历程,没有读者也无所谓。


注:本文全凭记忆复述,和事实或有偏差。如以后发现错误之处,会尽量修正。顾谢绝转载。

那些日子(目录)

April 27, 2008

游戏数值公式的表象与本质

周末、睡的比较晚,起的也比较晚。总的来说比平常睡眠时间长一些,所以到了这周一的第一个小时,我还是全无睡意。随便写点什么,关于游戏的,关于游戏设计方面的东西。

鉴于我在游戏设计领域还没有什么建树,远不如游戏程序设计方面有发言权。甚至对游戏设计说三道四的话,还不如在软件开发上乱侃几句有分量。在下充其量也就是个没吃过猪肉天天能见着猪跑的家伙,如果有猪肉已经吃腻味的方家读到这里,请一笑了之,别跟我一般见识。

本来在本文成文前打的腹稿中想举出些实际的例子来,可是发现怎么都会或多或少的得罪圈子内的策划朋友。罢了,只说些空话好了,算我做了半个月游戏数值,闲暇时发的点牢骚。

从为我们的游戏设定角色基础属性,以及设计战斗伤害的计算公式开始。

计算机里的一切问题都是数学问题。

这句话可能不太确切,但是目前一切利用计算机解决的问题,都必须先表达成数学形式,我想没有太多人会有意见。计算机游戏本质上也是一段交给计算机运行的程序,必须先转化成数学形式。这个转换过程无论是交给游戏策划来做还是给程序员来做,都无可避免。

转换的过程必然有损于原始设计初衷;策划做的时候往往不太能掌握数学语言,有失准确;而程序员总是刻板的追求形式上的统一,而失去细节(神韵?)。大多数人都趋向于游戏的表象而忽略本质,这不单是某一个人或某一类人常犯的错误。

扯点时间久远点的例子。我们知道中国人比古希腊人更早发现了勾股定理,但是西方人依旧把直角三角形直角边的平方和等于斜边的平方称为毕达哥拉斯定理。

杨辉三角形为每个程序员所熟知(大部分编程入门教材上都会用这个做编程习题),但是数学上,我们用的更多的对应名词是牛顿二项式定理,虽然中国人早了至少 300 年发现它。

抛开民族自豪感这些东西,当我们仔细比较上面两个例子中前后两者对同一数学定理的见解,我们会发现,它们其实是表象和本质的差别。只有我们用演绎的方法证明理解数学定理时,才能看透本质,并做出推广。摆脱事物的表象,用符号工具单纯的研究他们之间的关系,正是我们许多人思维中缺失的能力。

在我很小的时候,读数学科普读物。曾经嘲笑过毕达哥拉斯因为不能理解无理数而杀死他的学生;也没觉得负数是一个了不起的发现,而欧洲的数学家到 16 世界还不承认它。

直到中学时,接触到复数的概念,才觉得数学的神奇。然后回过头来思考,为什么一个小的数字减一个大的数字的结果是负数?负数到底是什么?问这些个问题时,我才明白,绝对不能轻视古人。看清事物的数学本质是件多么不容易的事情。明白自己其实什么都不懂只是一个开始。

扯远了,还是谈游戏。

拿魔兽世界(后面全简写作 wow )举例说事,恐怕是唯一不容易遭人非议的了。我们注意到, wow 之前有很多网络游戏,甚至暴雪之前也有一款很接近现代网游的游戏—— diablo 的 battle net 版,都采用了一种让玩家展现个性化角色的设计。那就是随着升级,为角色自由分配属性点,再由属性点影响角色的能力。

我可以举出长长的例子证明这一点,甚至电脑 RPG 的鼻祖,D&D ,也是有自由加点的设定的。虽然 D&D 里玩家可以加的点很少,暴雪在 diablo 里加强了这个设计。

为什么在这么多已有的网游中,大家都沿袭了这个设定,把它作为提供玩家个性化的必要手段时,暴雪绝然抛弃了它,把个性化转移到了天赋系统中。是为了创新吗?我不是暴雪的游戏设计师,我没有答案。但我们知道,暴雪从来不是一个以创新为名的游戏制作公司。而天赋树从外观上看,就是 diablo 中技能树的延续。

熟悉 wow 的玩家可能会说,是为了平衡。没错,但这是表象。我们需要进一步看清本质。

前段试玩了一个新出的网游,工作需要。可能是我已经厌倦了 wow ,对 wow like 的东西更提不起精神来。我试图说服自己,我觉得这个游戏比 wow 还无趣,是因为早先年打 wow 打累了。一丁点不如 wow 的地方都会被放大来看,甚至比 wow 做的好的,也会被忽略掉。

在公式化的游戏进程里,我玩到了游戏设计者“体贴”玩家的地方,比如升级飞快,轻松的接交任务等等。但是让我能理解,又觉得不可理喻的地方是,游戏依然保留着“传统”MMORPG 的设定,那就是每次升级都可以自由分配属性点。

亲身体验后,更加坚定了我的想法:自由分配属性点在大多数游戏中是一个极其鸡肋的设定。

或许是大部分游戏设计师认为,我们要提供给玩家一些自由度,而用户接受了自由加点这个设定,那么这就是一个廉价稳定的方案,我们也要提供它,没什么坏处,不是吗?

其实,大多数用户根本不知道他们真的需要什么,只是服务提供商对他们反复强化一些东西,这些东西反过来才成了必须品。在软件行业中,这样的例子比比皆是。“不要听用户的”是我去年听到的深合我意的一个声音。当然,千万不要曲解这句话。我从来没有否认过,用户调查,市场分析,等等这些对于游戏开发的重要性。

作为游戏设计人,我们到底想提供给用户多少自由度,怎样的自由度,这些都必须在设计时考虑在内。我们不是神,可以创造出简洁的自然法则构建出如此复杂的世界,任其发展。而且上百亿年了,其中的智能生物都无法完全了解这些规则。

说回属性点自由分配的设计,看似自由,比如一个小角色从1 级到满级可能得到上十点甚至上百点自由分配组合在 4 到 6 个不同的属性上。实际上,真正有用的组合方式寥寥无几。有天真的设计师,指望玩家可以摸索出超出自己想象的自由属性点组合方案吗?如果真的出现了,那么对游戏平衡一定是一个灾难。

如果你有信心对你没有预料到的东西做出合理的判断(不至于引起灾难),那么只有一种可能,那就是你做出了数值背后更深层次的规律的探索。可惜,大部分设计师没有这么做。

总结一下。如果你给了玩家一些自由度,让他们自由组合一些东西,达到个性化的目的,那么在设计阶段,作为设计师的你,就一定要全面列出所有的组合可能,并一一对其审视。

几十上百个的属性点自由分配的组合方案,从绝对数字上说实在是太大了。没有人会一一排到纸上演算。实际上也没有这个必要,因为大多数组合间是有规律的,不至于把量变堆积为质变。大量的组合方案在对玩家来说都是无意义的,没有人会那么组合,除非他犯错误,否则一定能找到一个达到他期望目的(PvP PvE 或者别的目的)的更优解。

从我信奉的 KISS 哲学上看,允许这么多理论上的属性点组合方案本身就是一个错误。如果我们希望玩家有 10 种个性化方案,就应该用最少的元素提供出 10 种这个数量级的变化规则。而不是提供上千种,让玩家从中选出 10 种。网络游戏中,提供自由度而存在的规则和设定不是为了考验玩家的智力(有时连智力都不需要,只需要机械性的忍受力)而存在的。

我相信很多人在接触一款新游戏时,都有过和我一样的迷茫:不知道升级后点该怎么加,只是无谓的犹豫不决,或是干脆一古脑把点全加在最需要的方面(比如物理攻击职业把点全堆在力量上)。


再来看看,大家沿袭最多的 MMO 系统,还是暴雪的——装备系统。很多玩家迷恋于装备上 +20 力量,+10 敏捷这些属性点加成。

我想大家在开发网游的时候,也自然而然的把这个系统做进产品里去了吧。

提一个问题:为了暴雪在做的时候,不把 +20 点力量直接写成 + XXX 点伤害力呢?这样玩家不就可以直接知道这个东西对他有什么好处了。

在 diablo 的年代,属性点意味着高级技能学习的先决条件;而 wow 中,同样的属性点,对于不同职业的实际效果反应是不一样的。比如敏捷可以增加盗贼的攻击力,而对战士则没有这样的效果。即使是同样依赖力量增加战斗力的职业,一点力量意味的伤害力的增强也是不同的。

所以,本质上,wow 中属性点到实际伤害计算公式中参量的变换,是为同一装备对不同职业的效能差异服务的。如果 wow 中,每个职业都用他们各自专有的装备,互不影响,那么,属性点的设计就是多余的。至少没有必要展现给玩家看,只让它作为设计战斗公式时的辅助工具就够了。


几年前,我曾经跟一个同事争论过一些游戏数值设定的方法。我坚持认为,一切都是数学公式,我们在设计时完全可以用 A B C D E 代替我们想要的东西,甚至一些公式也不用先定好具体函数是什么,而只需要根据最终的需要反过来推导。反正最终都是一些初等函数的变换,不可能跳出大框价。

但是很多设计师总期望先从现实中或是小说设定中找到对应物,非搞清楚为什么力量影响攻击力,根骨导致 HP 总量更长。那些名词到底是什么更为重要。以此可以展开更多的想象。

我说的名词还包括,“法师”、“战士”、“盗贼”、“牧师”这些……

最近我自己在做数值设定了,亲身感受后,果然合适的名词的确能辅助设计。没错,我们总需要借助一些更有意义的词来在头脑中建立起概念。但是,最终我也发现,一旦能克服对这些名词的依赖,纯数学的构建出基本的关系,无偏袒的,只为了数字上的平衡和变化去设计整体的框架。后期再根据需要找到合适的词并套入系统,以此展开细节上的联想,效果可能更好。更能帮助我们跳出玩过的前人的总总游戏的框架,找到新的东西。


不知不觉写了两个多小时,实在是太晚了。好象又什么都没写,等以后有闲了再补充一些实质性的东西吧。btw, 今天吃过晚饭后,在夜市上淘到一套对折的《说文解字注》。家里原有一本,是父亲的藏书。读中学时拿出来看,不小心弄丢了。这次撞见,挺高兴的买了下来。说是对折,但价钱还是今非昔比咯。

April 21, 2008

不那么随机的随机数列

曾经看过这样一种赌徒的策略:假设在一场赌大小的赌博游戏中,赔率是 1:1 ,而庄家不会出千,开大和开小的概率均等(皆为 50%)。赌徒一开始压一块钱,如果他压错了,那么下一次就压两块,再错继续加倍。一旦压对,赌徒永远可以保证有一块钱的进帐。并且从 1 块钱重新开始。

看起来,这种策略能保证永远包赚不赔。但实际上为什么没有人用这个方案发财呢?

放到现实中,试图采用这个策略去赌博的人,几乎都会赔的倾家荡产(当然只要是赌博,差不多都是这个结局)。不要怪运气,不要怪庄家出千,因为这个策略从一开始就注定了失败。

让我们把赌博游戏换成等价的扔硬币实验。请问,连续掷出 7 次正面的概率有多少?

稍有概率常识的人都可以心算出答案:1/128 ,也就是略小于 1% 。比 D&D 跑团时投出 20 重击要难多了(拜一下骰子大神)。

那么,谁能凭直觉说出,掷 30 次硬币,至少出现一次“连续 7 次正面”的概率有多少?我写了个小程序计算了一下,答案远大于大多数人的直觉,居然达到了 18.3% 这么高。

好了,现在把掷出正面换成压对大小。也就是说,你参加 30 次赌局,出现连续 7 次压对,或连续 7 次压错的概率并不那么的小。而且这个概率还会随着赌局次数增加,逐渐趋近于 1 。

这意味着什么?

如果你只有 128 块的赌本,在 30 次赌局中,输光的可能性居然有 9.53% 这么高。诚然,你可以运气很好,在一开始赢到一些额外的资金。但促进最初的策略所需要的进一步资金是 256 块,在 30 次赌局中是绝对不可能办到的。

赌徒可以增加自己的赌本,让自己可以承受更多的连续失败。但赌本的增加将是指数级上升的,但对提高不至于输光的概率却很有限(线形增加)。只要他在赌场上玩上一通宵,多少钱都能输干净。

赌博最终就是看谁的本钱多,而不是谁的运气更好(骰子大神啊,请不要跟我绝交。谈谈数学而已,莫要当真)。如果你赌本比不过庄家,乘早收手吧。btw, 这话是写给众多转战 A 股的朋友们的。


我想任何一个具有理性思维的人都不会对赌博有太多兴趣。想必我的 blog 的读者中这样的人居多,其实我今天主要是想谈游戏的 :) 。

我们前段开始内测了一个卡牌游戏 (注:需要内测帐号的朋友请自己去官方论坛 申请,不要找我 :) )

在测试时,同事在50 张的卡组里放了 25 张生物卡。并认为,在游戏中每次摸新的卡,是生物卡的概率是 50% 。可是在实际游戏中,几乎每局都会发生连续 5 次都摸不到生物卡的情况。

一开始,我们认为系统的伪随机函数生成的伪随机数列不够随机。后来换了一个随机数函数,情况并没有得到改善。

今天我计算了一下,如果是掷硬币实验,连续 30 次中,至少出现 5 次连续正面的概率达到了 36.82% 。当次数增加到 44 次后,概率超过了一半,达到了 50.32% 。而我们的卡牌游戏,几乎每局都会有 30 多次摸牌机会,出现连续 5 次摸不到生物卡的概率其实够大了。经常出现这种情况,还真是怪不了伪随机数列的生成算法,或是洗牌函数。

写到这里,还有人不信邪。我掏出了我的 20 面骰 。在桌上做起实验。

我们规定,投出 1-10 算小,11-20 算大。一直投下去,直到出现 5 次连续的大、而后游戏结束。最后统计一共投了多少次。在没有进行游戏之前,有人估计可能每玩一局可能会投接近 100 次;可实际结果另他失望(更接近计算结果)。

我们一共做了三组实验,分别在 22 次,24 次,31 次结束了。

如果有朋友想试试,可以用硬币或麻将用的六面骰实验。


所以说,当你在打网络游戏时,如果某天发现某件装备的凋落率,或是合成率远低于官方公布的数字。请不要抱怨自己命不好,也不要怀疑系统作弊。若让程序员们产生一个特定分布的作弊随机数列,又不那么容易被人看出规律(不够随机)出来,难度和成本(CPU 成本)比采用系统的随机数发生器要大的多。比如使用 Niederreiter Sequence

最后附一张图片,我随手用一个 C 程序生成的。程序在图片的下方:

random.png

#include #include int main() { int i,j; int map[128][128]; memset(map,0,sizeof(map)); for (i=0;i<1000;i++) { map[rand()%128][rand()%128]=1; } printf("P1\n128 128\n"); for (i=0;i<128;i++) { for (j=0;j<128;j++) { printf("%d ",map[i][j]); } printf("\n"); } return 0; }

用任何 C 编译器编译运行,再用管道输出到一个 .pbm 图片文件中即可。它生成了 2000 个伪随机数,并作为 1000 个点画在画布上。

我们可以发现,很多点都碰在了一起(一致随机分布往往呈现出的这种集束现象)。这并非随机数产生的不好,而是一种常态而已。

反过来,开发人员真的想讨好玩家,可以做一个更“均匀”的随机数列。让游戏中的各种概率发生更符合“大众的直觉”。那么,考虑使用 准随机数列 (quasi-random_sequences)。不过要注意,计算量会增加很多。而且这样的数列并不随机,只是讨好玩家而已,跟物理世界中的随机性相差甚远。

在《科学计算导论》的中译版中,把 quasi-random sequences 也翻译作拟随机数列。这里有一篇介绍性质的 paper 可以参考。


9 月 11 日补充:

原稿中概率计算有误,今天修正过来。感谢纠正它们的朋友。附上我的递推公式:

如计算 N+1 次实验中连续 5 次正面的概率,可先计算 N+1 次的投掷实验所有可能出现的组合数为 2 ^ (N+1) ,再计算其中出现连续 5 次正面的次数 f(N+1)。

f(N+1) 可以递推得到:

f(N+1) = f(N)*2 + 2 ^ (N-5) - f(N-5)

其中 当 N==5 时,f(N)=1 ;当 N<5 时,f(N)=0

f(N)*2 指前 N 次中已经掷出 5 次正面的组合数,无论最后一次结果如何都计算在内。

2^(N-5) - f(N-5) 指前 N 次的最后 4 次掷出了 4 次正面,而从未掷出连续 5 次正面的组合数,此时只需要最后一次也掷出正面即可。

April 20, 2008

我的 20% 时间

据说 google 有个 20% 时间,员工可以在本职工作之外,拿出一些时间做点有趣的东西。我和一些朋友讨论过这样做的意义。我想,正是程序员尤其是好的程序员不好伺候,当他们不为了物质利益去工作时,也只好想些“歪招”了。做点有趣的事情,学点有趣的知识,可以让工作更带劲点,老板看似为这 20% 时间多掏了些工资,而实际上,促进了另 80% 时间的工作效率。况且程序员的效率可以天差地别(包括同一个人在不同的状态时的),一不小心就赚到了。

虽然我自己不承认,但是客观的说,这半年看起来工作压力有点大。很明显,我把前几年坚持的健身活动停掉了,室内抱石基本没碰,户外攀岩就更别提了;很久没有骑自行车远行,桥牌没打、围棋没下,杀人游戏提不起兴趣,前段时间玩了次 OUAT (不明白的同学自己 google ),事后也觉得太浪费时间而没有继续。

老妈昨天来信(贴了一块二邮票的平信)千叮万嘱注意身体,我却不小心又在上周通宵了两回。好吧,等项目做完,一定好好改改作息时间,恢复每周 5 小时的身体锻炼。争取再谈个女朋友,执子之手,与子偕老。

不过,那也是项目做完后的事情,不是吗?年轻嘛,再奋斗个一年半载好了。老爸四十出头的时候还把床搬到办公室几周几周的不回家一个人在单位写程序,想必是遗传 :D 我这儿还有一帮子兄弟跟着呢。

跑题了。其实我想写,虽然我现在是七天工作制,但是到了周末还是弄点有趣的东西玩玩的。2/7 其实远大于 20% ,只是借 google 的 20% 说事而已。下面随便写写周末玩的些小玩意。

周五的时候一个老朋友打电话过来聊一些技术问题,他的项目(一个已有上亿用户的 IM 软件)想做第二版,看看我有什么想法可以沟通一下。当然我推荐了嵌入 lua ,以及希望采用一些跨平台构架等等。也只是随便聊聊,我对 IM 的东西也是一知半解,不适合说太多。不过这突然让我对 IM 又有了点兴趣。

周末主要是研究 XMPP 玩儿,谁叫我是 google 的粉丝呢,google talk 用这个,我也就从这里入手了。btw, 新版的 popo 也部分支持 XMPP 了,至少可以跟 google talk 互通,相互可以加好友。不知道有没有一点我的功劳 :) 反正我是成年累月的跟他们组的人嘀咕的。

一开始看了下 tessa ,一个用 C++ 和 lua 做的 Jabber client 。项目目标跟我期待的比较接近,而且是用 lua 去粘合的,比较对我的胃口。不过 checkout 了源码后,仔细读了一下,还是不太喜欢。可能我现在对 C++ 的东西都不是很感兴趣的缘故吧。不过它对 gloox 做了个 lua binding 有点意思,我本来想利用这个现成的 binding 在上面堆点玩具代码的,可是在没有文档的基础上去乱试进展不大。btw, tessa 在使用 lua 的地方有可圈点之处,虽然向 lua 传递数据的地方做的不够好(每次都生成一个 std::map ),但是在消息分发处的设计有那么点意思。

而后直接 checkout 了 gloox 自己玩。不过很可惜,弄了好久都没有成功登陆 google talk 。期间研究了下 gloox 的设计,有几个地方挺巧妙。毕竟是 C++ 做的,可以弄出花来。看过记下了,只是我还是喜欢 KISS 一点。

最终还是用了 google 的 libjingle 才真正的可以堆出点小玩具出来。在 windows 编译它颇费了点功夫(unix 平台下则简单的多),搞明白它的设计后,弄个文字聊天的 bot 出来只需要几十行代码。libjingle 的 example 中带了语言聊天和文件传输的例子,都可以顺利工作。

libjingle 用了个多线程的模型,主要是基于消息队列做的线程间通讯。整个代码应该算不错,只不过风格我也不太喜欢。


弄这些个东西,主要是想做两个小玩意。

一个就是服务器的错误报告,当服务器不状态正常时,可以由一个 bot 把消息直接发到我的 IM 上来,并且可以自动的发送相关文件。当然这个对习惯用邮件的人来说意义不大。我就是其一,不过如果做出来还是满有意思的。

第二个就是做个东西取代现在 blog 右边的 google talk chat back 。现在 google 自己提供的这个不太好用。每次有人点了后,只是在 google talk 上发个消息通知一下,还必须让我主要去打开那个 flash 版本的 gtalk 激活。

我希望做个 bot 可以直接跟 blog 的游客聊天,当然不是由 bot 自动回复,而是让 bot 做为一个 web 到桌面我的 gtalk 的一个代理。让我可以在桌面的 gtalk 上,看到所有的匿名访客的聊天信息都是从 bot 那里发过来的。而我可以直接回复,并用 /1 /2 /3 ... 这样的形式切换给谁回复。托 gtalk 的福,可以轻松的实现离线留言和服务器保存记录。btw, gtalk 允许同一帐号多处同时登陆,也方便了做一些事情。


想归想,把东西实际做出来还是颇需要一些精力和时间的。gtalk 使用通用协议固然是好事,方便了大家在其上做出好玩的东西。但是通用协议的繁杂也是无可避免的。弄起来并不轻松。

据我所知,我们的 popo 之所谓迟迟未用 XMPP 或是 SIP 这样的通用协议,一定程度上也是因为其繁杂。正如前面提到的那个老朋友,他的项目也没有完全使用 SIP 而是在 SIP 基础上做了一些改动。

回头来看我桌面上的 todo list ,还有好多有趣的事情想以后慢慢做呢。周末玩玩后就这么放着吧。明天继续做回主要工作。噢,这段时间在调游戏里的数值,又温习了点快遗忘的数学知识。比如泰勒(Taylor)展开式等。这些等下次有兴趣了再写写吧。

April 18, 2008

招行的系统测试过吗?

早上收到一条短信,招商银行发的,说是给我的信用卡信用额度上调。附带邀请我给慈善基金捐款。

上次在网上看见招行的广告,在网页上募捐未果。(招行的那个募捐页面似乎只支持 IE ,我尝试了 opera 和 firefox 后作罢)这次看见回个短信就 OK ,按提示输入了个 Y100 回复,打算捐个 100 块试试。

结果答复却是“查询码介绍:……”,分明是不认识这个捐款指令 (._.!)

唉,让我说什么好。估计是系统还没测试好就上了吧。而且使用它捐款的人太少,失败了也没人抱怨,也就得不到用户反馈了。

虽然国内的银行我只用招行的,招行在许多方面也的确做的比其他几个大银行好。但不得不说,有时还是有点恨铁不成钢的感觉。回想上个月招行的客服电话回访,让我提提意见,我也就说了一条,让网上银行支持非 IE 非 Windows 平台吧

April 10, 2008

游戏的帧率控制

我曾在不同场合,多次向人表达我对游戏程序设计的一个重要观点:时间控制,对于游戏程序设计至关重要。

这是因为,大多数电子游戏,都是一个实时人机互动系统。在非人机互动的软件中,软件及硬件一起是一个封闭系统,时间参数对这个系统是否正确工作是无意义的量。这样的系统,我们尽量应该设计成自动化工作模式,只要有正确的输入,得到正确的输出即可。通常,这样的系统的开发,还应该有自动化测试的流程去驱动它。

但实时人机互动软件则不一样,我们得把人看成系统的一部分。人和机器的交互是通过人眼、人耳、键盘、鼠标等完成信息交换的。如果把人脑看成一个线程、计算机看成另一个线程,我们几乎没有能力实现一个资源锁,利用锁的方式保证系统工作永远正常。人脑在全速运转时,适时得不到正确的信息输入,整个系统就可认为出现了故障。

可见,在这样的系统中,时间控制显得多么的重要。


当我们提到游戏的帧速率,我想把逻辑帧率与渲染帧率分开来处理。在这个问题上,或许许多游戏引擎的开发者并不赞同我的做法。但是,这毕竟是一个可以简化问题复杂度的方法,个人以为值得推广。

所谓逻辑帧率,就是游戏引擎每秒处理逻辑事件的频率。在我参与的游戏项目里,通常,我们都将逻辑帧率固定在一个固定值。为了方便直觉的构想,多数情况下,我倾向于方便十进制计算的数值。比如 100 fps 、50 fps 、25 fps 、20 fps 等等。

限定了逻辑帧率后,我们可以把人机交互系统中的机器剥离出来,成为一个独立系统,这样在测试和设计时,时间的要素被弱化了。游戏逻辑可以被严格限定在第整数个帧内发生。

通常,视频游戏软件的大量时间都消耗在渲染图象中。而逻辑处理在今天的计算机上,可以更容易的保持在一个高帧率,以提高拟真度。注意,这里所谓的逻辑处理,还包括了虚拟世界中的物理状态的模拟,对于简单的游戏,可能仅仅是对象的坐标;复杂的 3d 游戏或许就包括了物理引擎的运算。

对于渲染的问题。个人认为人脑处理信息允许在很短时间的内出现一点偏差,系统会自我纠正。那么只要计算机输出的信息的时刻不要差的太远即可。视觉,虽然作为最大信息量的通道,需要保持较高的通讯频率,但我们并不需要无限的高。渲染帧率高于逻辑帧率一般是没有什么意义的。

当很多人埋头一味的追求高的渲染帧率时,我们是不是应该停下来仔细想想,怎样可以用最节省的方式让大脑适应你的游戏画面。最终是你的大脑在判断画面是否流畅,而不是渲染帧率那个数值的高低。

相信大家都看过很多欺骗眼睛的图片,网络上已经有很多了。许多静止的图片会让人感觉在动。一些运动的图片,实际的动作跟像素级别上的变动不太一样。

简单说来,人脑接受和处理图象的速度有限。大脑在处理此刻视网膜上的图象的同时,还会对未来这些图象的变化有一个预期。当图象的运动满足这个预期时,我们会感觉很流畅很舒适;当和预期不符时,或者让我们头晕眼花,或者让我们产生错觉。(btw, 我忘记曾经在哪看过一副瀑布的 gif 动画,这组动画经过特别处理。当瀑布静止时,我们的眼睛会产生水向上倒流的感觉。)

所以,无论是 3d 游戏还是 2d 游戏。快速的移动镜头都是不提倡的。不光是游戏,电影中,我们也极少看见导演快速的移动镜头,大多数场景都是固定镜头拍摄的。

如果你非要移动镜头(比如让镜头跟随主人公),那么也尽量让移动速度表现的足够匀速。因为我们能达到的帧速率有限,快速移动镜头很难做到线性(无论是 2d 还是 3d 游戏都很难做到),大多数情况下,我们会在设计上放慢镜头的移动。

最后,谈一下镜头移动和渲染帧率的控制问题。

既然人脑总希望画面符合预期,我们在引擎呈现的时候,最关键的就是在最准确的时刻渲染出准确的一帧。如果我们一味的在系统空闲的时候渲染,往往会出现在很短时间段内,时快时慢,虽然快慢相差不多(可能只有 0.01 秒的差别),但也会影响到人脑的感觉。

一个简单的办法就是,把渲染帧率也锁死。比如锁定为和逻辑帧率相同,或为逻辑帧率的一半,1/3 等。在多任务操作系统环境中,这样可以让你的游戏跟别的应用程序良好的协同工作。当游戏尽量的把空闲时刻让出时,它也更容易的在准确的时刻渲染时期待的画面出来。

btw, 我们目前的引擎使用了一个简单的渲染帧率控制函数。侦测最近几秒的渲染情况,一旦发现过忙,则主动下调渲染帧率,让渲染可以均匀的进行;当系统比较空闲时,再上调渲染帧速率。实际运用下来,效果不错。

April 06, 2008

负反馈系统在模型动画控制中的应用

最近在解决 3d engine 接口的一处设计问题。我们知道,在 3d 游戏中,engine 的接口设计往往比性能更加重要,这决定了 engine 是否好用,甚至是否能用。简单的功能堆砌不是 engine 。

目前我想弄清楚的一个技术点就是,当模型置入场景后,如果播放的动画本身有位移,引擎应提供怎样的接口,让使用者最为方便的表达他意图。

具体到一个问题点来说,当我们的美术人员制作了一组四足动画奔跑的动画后,怎么在游戏中最自然的表现出来。

就我有限的眼界所知,有许多人在制作 3d engine 时是这么规定的:美术需要把动画调整成原地奔跑的样子。(或者换个方式,在从制作软件中导出时,让原本具有位移的动画变为原地移动)如此,就可以在最终表现时,自由控制播放的速度。

但是稍加思考,就知道这样做导致的图象表现和我们期待的样子是有偏差的。下面让我们来分析一下。

生物在奔跑的时候,重心的移动线速度不可能保持一个完全均匀的速度。脚掌在着地的那一刹那,产生了极大的推力,身体拥有了最大的加速度。但速度本身却是在腾空那一刻加到最大,而到下次着地的过程中是做一个减速运动的。另外,重心也非保证完全平行于地面运动而是有上下起伏。

硬把跑步的动作拉回原地,而在用的时候拉成匀速运动是不准确的。如果美术提供的素材足够真实(比如使用了动作捕捉器)。那么这些变了形的动画信息在游戏中就会被表现成身体在地面滑行,而没有跑步的力量感。

如果让 engine 直接按带位移的方式播放走路、跑步(可能还有格斗时的各种腾挪)动画,那么这些动画实际在世界中移动的距离就需要被了解并参与计算。尤其是有些动画,并没有播放完整一个循环,就被中间打断(往往是改变了移动方向或插入新的动作),导致无法预知其中间状态。

我们在引擎早期设计时,曾经考虑过导出动画信息中的位移信息(其实还包括模型的角度信息,一共是 6 个自由度)。期望中间层可以利用这些信息将动画表现的更真实。

可是在编写代码时,我们发现,利用这些数据并不容易。

经过一番思考后,我发现应该换一条路来解决这个问题。

如果我们了解一丁点自动化控制的理论,就可以知道,提供一个“负反馈”的机制,对系统稳定运行是非常重要且必要的。开环系统远远不如闭环系统稳定,是我在刚进大学不久,课堂上学到的一条重要知识。

其实我们完全不需要知道美术制作的跑步动画,模型在一个循环中移动了多远,节奏有多快。更不需要知道每个时刻动画相对于起始帧(或上一帧)做了怎样了状态(位置和方向)改变。

我们只需要通过接口让 engine 播放指定的动画,并可以随时取得模型的状态信息就够了。也就是说,指定 engine 播放一匹狼奔跑的动画,那么不下达新的指令前,engine 就一直不断的让这匹狼向前奔跑就好了。另外我们需要的是可以查到这匹狼的坐标位置,并可以控制改变动画播放的速度。

注意,这里改变动画播放的速度,并不会改变狼奔跑中每一步跨出的距离,而只是改变了步伐的节奏。

我们不断的测算狼当前的线速度(这可以通过记录上一次查询的时间和坐标,同当前的量相减,并对过去一段时间的测算量做一个加权平均即可。如果发现速度达不到期望的速度,那么就把动画播放速度提高一点点;反之就降低一点的。

最终,这匹狼的奔跑速度就会稳定在我们期望的值上,或在这个值的周围做小幅度震荡。

除了位置的控制外,我们还可以讲角速度也纳入负反馈系统,即可以得到一个相对真实的转弯动画(或许要配合美术更多的资源)。这些道理相通,就不展开写了。

总结一下,最终我们需要的是 engine 提供一个动画播放并切换到其它动画的接口,并在内部完成动作融合。btw, 相关的问题前段时间我写过一篇 blog

engine 根据动画信息,负责修改模型的位置以及角度等状态信息。也就是说,动画数据不仅仅是用于表现,还会反作用于模型的状态属性。

需要可以通过 engine 查询模型的状态信息,包括其坐标(相对父节点的坐标),朝向角度。

可以自由的控制动画播放的速度。


我相信,一个优秀的 3d engine 是在这些细节设计的推敲中诞生的,其重要性绝不亚于去应用新型号显卡的高级特性。或许已经有很多制作 3d 游戏或引擎的人已经这样做了,但我也相信,还有人没有这样做,有如想通这个问题前的我。故作此文记录之。

April 03, 2008

记录几个近期碰到的 bug

自己的代码若出了问题,大多数情况我会重写。只要模块划分清楚,设计做好。重写的部分都不会太多。但是别人的代码出问题的话,很多情况下,就只能硬着头皮耐心找了。这就是我这几天的境遇。

前几天我们一个系统更新升级,在公网上一直不太稳定。这次产品上线有点仓促,不过也是内部测试过的。一直没碰到什么大问题,而在公网上情况毕竟复杂的多了,而且排错的压力也比较大,毕竟为玩家服务的程序,公布后就不能随便停掉收回来。这就依赖热更新,在运行期间查错了。

先说昨天晚上让我弄了一通宵的 bug ,留个记录,对以后可能出现类似的问题起点警示。

原本系统已经稳定运行了大约 18 个小时,白天都相安无事。项目理论上不算我负责,不过我实现了其中一个模块,已经用到了我们主项目中已有的一个模块,所以我也一直盯着观察运行 log 。

到了半夜,打算烧水冲个澡然后回家睡觉。在等待的空闲时间,一边随便翻着宇宙学的科普书打发时间,一边瞟着自己实现的模块的 log ,突然发现一些异样。

虽然数据包还是合法的,但似乎输入的数据有些不合常理。一下来了精神,想弄清楚是怎么回事。自己检查了系统后,发现系统已经不能正常工作了。由于已经到了零点,办公室就我一个人,想必同事基本都睡了。我决定自己搞定这个问题。

花了点时间翻阅了所有子系统的 log ,追踪到不正常的数据包的流向。由于大部分代码我事前都没有阅读过,对照着代码看有点辛苦。

不正常的表现是,在某种特殊特定情况下,会有一些不正常的数据流过我的系统。但是在 log 上,没有哪个系统曾记录发出这些数据包。了解了数据流向后,我阅读了所有怀疑会出错的子系统的相关代码(中间居然读到两处边际条件下可能出现的例外没有处理对,随手修改掉),都不认为是问题的根本原因。

这个问题其实前天晚上也发生过,可是昨天白天我提出后,每个人都不觉得自己的代码会引起问题。而白天时系统也的确很稳定,这让我很郁闷。作为程序员,我们都相信,既然 bug 出现过,就不可能自动消失,这种情况发生,让人心里有个疙瘩,非常的不爽。

之前有同事甚至联想到是不是系统出了问题,因为在内部测试时从未发生。我本坚信一定是我们自己的实现出了问题,坚持了一天后,到这后半夜神志不清时,差点动摇了。

中间辛苦查错的过程就不详述了。只能说被百般折磨,头晕眼花后,最终在上午和今天上班的同事一起定位了错误根源:

我们是多进程的系统,其中有个子系统拥有一个管理器,管理器以产生子进程的方式引导下级子系统。在 fork 之后,它关闭了自己的标准输出文件句柄。子进程原本没有用 printf 做 log 输出,也就相安无事。可它用到前任一个同事写的一个程序库,在进程异常退出时,可能用 printf 输出一些东西。btw, printf 是 CRT 库中的,在用户空间有缓冲。在进程退出时,会刷新这段缓冲区写到标准输出中。这也是有些使用 fork 的多进程程序有时候会发生重复输出的原因。

因为标准输出已经关闭,但管理器和外部通讯用的 socket 却在 fork 子进程后漏关了。而这个重要的 socket 了复用了标准输出的文件句柄号,这样悲剧就产生了:在几个大的子系统间的通信被一个本该被隔离的进程误插入了一些数据进去,而谁也说不清是从哪来的。而面对几百 M 的 log 文件,偶然发生的这个异常,大部分被过滤扔掉,不会造成太多负面影响。藏在里面的 bug 种子却很久不被人重视。

直到另一个系统处理异常情况的边界条件被触发,测试期没有被覆盖到的分支上的 bug 被引发,它们在一起造成了系统不能正常工作。

ps. 这次事故除了在项目管理和代码管理上给了我一些教训外,能记录下来提醒后来者的技术点是:不要轻易的关闭标准输出,而把它重定向到空设备就可以了。


本来想继续谈谈另外几个 bug 的,写到这里没啥兴致了。问题找到,可以安心过清明了。祝大家节日快乐。