« June 2007 | Main | August 2007 »

July 28, 2007

十年圆梦

中午睡眼惺忪的时候,老丁打了个电话过来。说是南怀瑾南老师周末在江苏开堂讲课,问我有没有兴趣去听。我是很喜欢读南老师的书的,可惜的是,今天必须回武汉。真是遗憾!

小时候的玩伴 DSH 明天举行婚礼,他从法国回来办这桌酒席。相比之下,我飞回武汉就算是近的了。

还在 apple II 时代时,我们在一起学习 BASIC 语言。前些年混在北京时学了个名词叫做“发小”,应该指的就是我们这种关系。初中的暑假,一个人在家写程序没意思。或者几个人约好了跑去学校机房,或者大家去 DSH 家里泡上一天。他家最为宽敞,家长可亲,一堆人闹个天翻地覆也没关系。

那个时候,我们一起凑钱邮购软盘装的软件,订阅私人油印的计算机杂志。还比赛在苹果机上用 basic 写小游戏:俄罗斯方块、双截龙之类的,那是我玩计算机最快乐的一段时光。除了编程和游戏,我们还传看《时间简史》,讨论些关于六维空间的“高深”话题,把教室的黑板涂满,幻想时间旅行会是怎样的形式。

再后来,我们几个人中考又考入了同一所重点高中。一群人中属我的成绩最差,也只有我一个人坚持搞信息学比赛了。高考后大家便分道扬镳。

一伙人里,我和 DSH 交情最好。大学里写了不少信,大多是学习编程的一些心得。主要是因为当时的大学同学中没有同水平的人交流的缘故吧。还有就是他一直替我上 bbs :当时我对 cfido bbs 尤为热情,每周让 DSH 用我的帐号拨号上去(寝室里没有电话,而他在武汉读书,周末可以回家),把信(帖子)打包收下来,装到软盘中邮寄给我。然后我把回帖写好,拷入软盘寄回给他,帮我再上传。不知道我这种泡 bbs 的方式,算不算中国第一人了 :D

97 年的夏天,我在学校里打听到一个活。为广东佛山一个小图书馆开发一个软件,要求用 delphi 做。我当时对 delphi 一无所知,在书店买了本 21 天精通系列,就约 DSH 上了路。身上连回程车票的钱都没带。

两个人从广州下火车,转汽车到佛山。在佛山的街头迷路,四处找懂普通话的人问路。而我们唯一会说的一句粤语就是“图书馆”。走在佛山的马路上,大家都是第一次独自到一个陌生的城市。他说:这种感觉真好,整个城市只有两个人相互认识。我们甚至没有跟对方事先联系,提前了一周到,弄的那个图书馆的馆长有点措手不及 。当时这种课外打工的事那个年头在学校里很吃香,早有高年级的学长接下来。其实,我们就是打算提前出击,赶在别人来之前把东西做完,领了报酬走人。

后来两个人关在一个装修特别好的会议室里干了一个星期,我装了个 delphi 1.0 边学边干。只记得屋子里的音响不错,那个时候光盘装的 mp3 刚流行,任贤齐也刚在大陆火起来。听了一个星期“心太软”的 mp3 ,从不唱歌的我也会哼几句了。最后,一个人分到了几百块的报酬,不如开始许诺的多,但依旧美滋滋的。回家时还奢侈的买了卧铺。

那是我人生第一次靠自己编程赚到了钱。

再后来大学毕业,我便头也不回的离开了校园。毕业那天指着天说,这辈子再也不要考试了,然后四处飘荡。而 DSH 则一直读到硕士,然后去了法国读博士。小时候的玩伴都分开了,据说另一个家伙在德国读完书,已经入了洋籍,变成德国鬼子了。留在国内的一个在上海交大读完书找了份专业对口的工作做硬件芯片。看他早早的在寸土寸金的上海滩买了金屋藏了娇妻,就知道小日子过的挺美满。最终只有我,落的孤身一人,不是在公司,就是在去公司的路上。还美滋滋的不求上进,只要有程序写就好。租来的旧屋子里只求有张床,床头放几本书,两三部游戏机,乐的屁颠屁颠的。

DSH 说,一定要带上女友或是老婆赴宴。你小子如今丰衣足食,又是玉树临风,说没藏个 mm 在家里也没人信啊。我说,我倒是想,早些日子可能还能找到 mm 一起陪我去喝你的酒。但是现在真没有啊,让我去哪变一个出来?


04 年的时候,冬季校园巡回招聘,我特地把最后一站设在长沙。招聘结束后,就乘公交去火车站,买了张站台票,冲上一辆京广线北上的绿皮车。火车临时停在岳阳站的时候,天上飘下了鹅毛大雪,大片大片的雪花从车窗的缝隙飘进来。车厢里很空旷,我就穿了件薄薄的夹克,一个人蜷缩在窗角,看着雪花在衣服的褶皱里融化,仿佛回到的大学时代。

刚读大学的时候,武汉到长沙火车要六个半小时。绿皮的车,硬板的票。火车上的时间长度颇为尴尬,不够睡一个晚上来个夕发朝至。要么很晚上车,要么很早到,又或者一下午什么事情都干不了,在火车上发呆。人坐在车上超过 3 小时就会烦躁不安。有一次坐回家回家,身上没带什么钱,车厢过道里叫卖的五元钱一网兜的橘子硬是没舍得买。结果火车过了岳阳就开始晚点,迟到了四个小时。因为一开始打定主意不买车上的东西吃,下车的时候已经饿的眼冒金星。

那个年龄段恋家。坐在火车上我就想,什么时候咱坐飞机回家。多快啊,天上飞半小时就到了。又或者从武汉到长沙打一条直线隧道,不走地球表面的弧面,路程近还省动力。估计坐在上面跟过山车似的。

后者属于异想天开,这辈子也不会建设出这么个工程出来。前者由于火车提速,武汉到长沙的短途航线取消了。


今天是下午 6 点 40 的航班,跟上海那位金屋藏娇的哥们约好在武汉机场落地碰头。从上海走的航班会晚点到,他知道我不喜欢等人,走之前叮嘱我说让我耐心等一下。我打趣道,我有严重的 RPWT ,这些年乘了几十趟飞机,不准点的航班过半。所以今天他等我的概率应该更大一些。

上飞机前他小子还不放心,我们互相发了消息说已经上登机了才安心。结果到了八点,飞机抵达武汉上空,窗外突然电闪雷鸣,电光照亮了半边天。闪电从云层上打下来,跟看科幻电影似的。伴随着广播通告,我明显可以感觉到准备下降了,甚至可以感觉到飞机已经放了起落架。可是机舱里好像过山车。间隙的颠簸失重让人快吐出来了。

环顾了四周,发现机舱里还没坐满人,突然脑子里闪过个念头来。幸亏我是孤身一人,若是真携美同行,人家在我这还没讨着什么好,若是就这样因我误了卿卿性命,那可真是不划算了。看来还是一个人好,生死都干干净净,想着想着,心里出奇的平静。

等飞机平稳的时候,又回到了万米高空。广播里说,我们将迫降长沙。

长沙落地时已经是九点,打开电话给家里拨了个电话报平安,据说武汉刚刚下冰雹了,很是恐怖。又发了一通短信,那位上海过去的哥们居然八点十分安全降落。还是提前到的,丁点事都没有。就在那个时刻,怎么就让我碰上在天上兜圈子下不去呢?电话里让他先回家,瞧这样子我不定什么时候能到。看吧,我就是说的不错,RP 不是一般的差。虽说中国的航班不准点也是家常便饭,但碰上我,误点率还得再加二十个百分点。

在长沙的停机坪上等着,机舱内满是抱怨。我旁边的 mm 嘟噜着,她的朋友告诉她八点的很多航班都准点抵达了。我心说,看刚才那情形,你是要性命还是要准点。

唉,人生也不缺这几个小时。


最后,我圆了梦,从长沙飞回武汉。

July 24, 2007

C++ 0x 中的垃圾收集

g9 老大的 blog 里最近写了篇 关于C++ 0x 里垃圾收集器的讲座 。这是我看见的第一篇关于 C++ 0x 标准中GC 的中文文章。

最近两年我对 gc 很感兴趣 :D 已经在项目中用了两年。项目从 C++ 转到 C ,gc 模块的实现发生了变化,但是本质却没有变。我对 C++ 加入 gc 是非常欢迎的,这点在以前写的另一篇 blog 中已经表明过态度。

记得两年前,当有机会当面问 Bjarne Stroustrup 关于 C++ 发展的问题时,我毫不犹豫的讲出自己对 gc 的迫切期待,并希望能够以最小代价的把 gc 加入 C++ 。因为已经实现过一些 C++ 的 gc 模块,我有一些语言上的需求。当时描述了自己的想法,可惜英文实在是太差了,完全说不清楚 :( 因为没听明白我的意思,Bjarne Stroustrup 他老人家似乎也很无奈,最后只是建议中国的程序员应该参于到语言的标准化事务当中去,一直以来,C++ 标准委员会中似乎没有来至中国大陆的程序员。

既然 C++ 是你的工具,你就应该努力把自己对工具的改进需求说出来。

其实我的需求很简单,就是 C++ 中应该加入一些对数据结构中数据类型的有限描述。其实只做内存管理的话,类型信息只需要区分数据还是指针就够了。具体数据类型可以忽略。

我的想法就是像虚表一样,给每个类多加一张表,描述这个对象中指针的位置(记录一个偏移量)。由于 C++ 的对象布局比较复杂,这个工作如果不在编译器里做,会相当麻烦。有这样的信息,gc 就可以容易的遍历内存了。看那篇文章的介绍,C++ 的 gc 似乎用 gc_strict gc_relax 这样的关键字来描述一整块内存区内有没有指针,而没有更细致的精确到每个数据上。这跟已有的 C 的 gc 库 实现类似。我猜测这些是为了兼容 POD 类型设计的,对于 C++ 自己的类,应该可以更好的解决。毕竟编译器知道全部的类型信息。

除此之外,遍历堆栈依旧是个问题,但已经好多了。遍历可以用各种语法糖来实现,反正 C++ 有了 template 后,什么诡异的写法都弄的出来 ;-p

最后说两句 gc 的效率问题。gc 没有人肉内存管理效率高是一种普遍的误解。如果不是靠臆测,而是自己实现一个 gc 模块,然后做代码剖析的话,很容易相信 gc 可以带来更高的性能。关于 gc 和人肉内存管理之间的性能话题,以前写过太多,吵的太多,嚼着都没味道了。今天就不再写了。

July 19, 2007

模型顶点数据的压缩

缘起:听天下组的同事谈起,游戏的资源数据量太大,导致了加载时间过长,影响到了操作感。最终不得不对数据进行压缩。

当玩家配置的物理内存足够大时,往往资源数据压缩会适得其反。因为多出来的数据解压缩的过程会占用掉太多的 CPU 时间。物理内存够大的话,操作系统会尽量的拿出物理内存做磁盘 IO 的 cache ,即使你的程序不占用太多的逻辑内存,而反复从硬盘加载大块数据,也不会感觉到磁盘速度的影响。当数据量大一个一个临界时,被迫做不断的外存内存的数据交换,这个时候,数据压缩就会体现出优势。因为解压缩的 CPU 时间消耗是远远小于硬盘 IO 所消耗的时间的。

btw, 玩现在的 MMORPG 往往配置更大的物理内存得到的性能提升,比升级 CPU 或显卡要划算的多,就是源于这个道理。通常 MMORPG 都需要配置海量的资源数据,来应付玩家的个性化需求。

天下组的同事暂时只做了骨骼动作信息的压缩。这个这里暂且不谈,我想写写关于模型顶点数据的压缩。记录下这两天在思考的一个有损压缩算法。

通常模型中的顶点数据分两部分,顶点坐标和索引信息。顶点坐标表示了每个顶点在三维空间中的位置信息;索引信息用来表示模型的结构,当模型网格用三角形表示时,每个面就用三个顶点索引值来表示。


先来看顶点坐标数据的压缩。

原始数据通常是 3 个 float 表示一个顶点。最简单的想法是用 half float 来压缩,这个可以得到 50% 的压缩比。half float 基数只有 10bit 的精度,对于游戏这种对精度要求不高的应用,倒是也够了。

不过我们用一个简单的方法,就可以在保障大约 50% 压缩比的前提下,提高精度。

只需要把所有顶点坐标信息归一化,即,让所有坐标值都放到 [0,1) 中。这样,我们再用 16bit 的 short 保存一个定点数,可以得到全部 16bit 的精度,而不是 half float 的 10bit 。这样需要付出的额外代价,只是多记两组缩放和位移的数据(6 个 float ,24 字节)用于还原到原始坐标。

这一步做了以后,其实我们还有进一步压缩的余地。

大多数情况下,考虑到游戏中每个最小单位的模型的顶点数少不过几十,多不过上千。归一化以后,16bit 的坐标精度对于屏幕显示都是多余(想想屏幕才多少像素精度)。下面我们想办法把 16bit 的数据压缩到 8bit 。

顶点的次序其实是无关紧要的,我们先对顶点进行某种排序,让最终的顶点数组中相邻顶点的相对距离都尽可能的短。考虑到顶点并非随机分布在单位立方体中,而是符合某种规律。这样的顶点序列通常可以找到。我们并不需要找到最优解,用贪心算法+模拟退火应该就够了。

因为只能保证整个顶点序列中绝大多数顶点的相对距离较短,而无可避免出现少量特例。我们需要允许加入一些冗余的顶点来缩短相临顶点间的距离。这样,可以让整个顶点序列遵循一个合理的限制:最终所有相临顶点间的任意坐标轴上的距离不得大于 0.5 。

ok ,求出这样一个合理的顶点坐标序列后就可以开始压缩了。

我们让每个顶点坐标都表示成相对前一个坐标的差值,大多数值都会是一个相对比较小的数值。从二进制来看,高位会有一大串的零。接下来把这些值分为 5 类。

  1. 至少有 9 个连续零在高位 (000)
  2. 至少有 7 个连续零在高位 (01)
  3. 至少有 5 个连续零在高位 (10)
  4. 至少有 3 个连续零在高位 (11)
  5. 至少有 1 个连续零在高位 (001)

由于前面提到的限制,最高位一定是 0 而不会是 1 。(差值的符号位单独记录,而不采用补码)以上五类数值可以用变长的编码来区分。(我已经标在上表中)

8bit 中除去符号位和类型标记,有 7/5/3 个连续零的坐标差值(小于 1/128 、1/32 、1/8 )可以有 5 bit 的精度,另两种情况则有 4 bit 的精度。我们可以把第 5 种情况:至少有 1 个零的数据统一认为是冗余顶点,这样方便在最终解码的时候剔除掉。(即在顶点跨度很大时,强制添加一个冗余顶点)

7 月 21 日 补充:云风对以上的算法的实现和测试结果附在文章最后。


索引数据的压缩则必须是无损的,惯例是用 short 来表示 index 值,限制单个模型的顶点数在 64k 或 32k 之内。最简单的压缩算法是用变长编码,比如小于 128 的索引用一个字节,否则用 2 字节。

我猜想,如果顶点的排列是有一定次序,比较临近的索引值对应的顶点构成三角形的概率也比较大。那么,我们同样可以利用临近的索引值的差值来做变长压缩。或许可以效果可以更好一些 :D


模型数据中还有法线信息、贴图 UV 坐标、骨骼信息和权重等。这些的压缩方案,今天就不一一展开讨论了。


7 月 21 日 凌晨 补充:

花了一天时间把压缩以上压缩算法初步实现了。

归一化并用定点数表示的这一步,精度损失在十万分之一之下。(这一点,根据 float 的有效位数,心算也可以估算)所以,不必考虑这里的精度损失。

压缩到 8bit 的这个步骤比较复杂,主要是找到一个合理的顶点排列。这个问题其实是一个 TSP(货郎担问题)的变形,我选用模拟退火法来解。

输入数据是从项目中任意选择的一个人物模型中的一部分(max 导出的模型文件中任选的一组数据),有 268 个顶点(构成了 423 个面)。这个数量级的顶点数颇具代表意义。

如果用 float 保存这些顶点信息,需要 268*12=3216 字节。

压缩后,添加了 30 个顶点,共 298 个。每个顶点用 3 字节表示,加上额外的 6 个 float 值,一共为 298*3+6=900 字节。

压缩比为 900/3216=28.0% 。

平均的精度损失为 0.08% ,误差最大的几个顶点坐标的误差为 0.2% 左右。我个人认为,这个误差对于屏幕渲染完全可以接受。因为这样一个模型最终渲染到屏幕上不过几十像素宽,这个数量级的误差反应到屏幕上,甚至不会超过一个像素。

如果顶点数更多一些的话,由于分布在单位立方体中的顶点更密集,我想误差还会再小一些。

July 17, 2007

X Window 编程的两个小问题

X Window 其实比 Widnows 要好理解的多,设计的也更为合理一些。但是无奈,资料太少、中文的就更少了。搜来搜去就那么几篇,书也没看见几本 :( 所以在 X 下做开发,对于我,比在 Windows 下麻烦了许多。

最近一段时间遇到了许多问题,解决了两个,记录在这里:

截获窗口关闭的消息

Windows 下很简单,WM_CLOSE 消息是也。btw, 一般人也不会理会这个消息大多数情况其实是由 explorer 转发过来的,而不是 GUI 系统直接发进你的窗口 。在你的窗口进程陷入死循环,无法处理窗口消息时,标题栏上的关闭叉叉按纽依然可以工作。

X Window 下,我想当然的从文档中找到一个叫作 DestroyNotiify 的消息,但是写到消息循环中怎么都触发不了。在 X 下,用鼠标点击窗口右上那个叉叉,得到的反应和 Windows 下点了死掉的窗口的关闭一样。会弹出一个对话框,OK 后强行杀掉。

后来才知道:应该先用 XInternAtom 拿到一个叫做 WM_DELETE_WINDOW 的 atom ,用 XSetWMProtocols 设置到窗口上。然后在消息循环中 case ClientMessage 方可查询到这个 atom ,从而得之窗口将被关掉的消息。

X Window 下的键盘自动连发跟 Windows 下行为不一致

当你按住一个键,X Window 和 Windows 都会模拟出一串的连续键击。但 X 下的行为跟 Windows 不同的是:X 会自动生成 key up 的消息 (X 下叫作 KeyRelease )。

在 Windows 下,你只会在真正松开键时得到唯一次 WM_KEYUP 。而在 X 下你会得到一系列的 KeyPress / KeyRelease 对,严格的按照一个 KeyPress 消息、接一个 KeyRelease 消息的来。大多数情况下,X 的处理更为合理。但是对于游戏就有点麻烦,我们无法简单的知道用户是否一直按着一个键不放。

这个问题我很早就遇到了,记得当初不知道 google 了些什么关键词就找到了相关资料。大约记得 X 协议中是可以关闭 X 的自动重复的。今天想把这部分代码补上,又怎么也 google 不到了 :( (该死的 google.cn 就是不给我记录搜索历史!)

最后终于还是从文档中翻出来了:

我们可以用 XAutoRepeatOn / XAutoRepeatOff 开关 autorepeat 的设置。但是简单调用这 API 似乎并不是一个好的方案。因为这个设置居然是影响全局的,甚至在窗口关闭程序退出后,设置都不会还原。

还好在 man XAutoRepeatOff 的时候看到了相关的另一个 api: XQueryKeymap :) 。用这个就够了,它可以查询键盘的真实状态。我们只用在消息循环中响应 KeyRelease 的时候,调用 XQueryKeymap 检查一下对应的按键是否真的被按下就 OK 了。

ps. 有没有 X Window 编程的高手啊,一个人搞这些东西总是很郁闷。

July 16, 2007

关于 jpeg 文档的修订

还在大四的时候,曾经跟 sina 游戏制作论坛的人赌气,自己实现了一个 jpeg decoder 。大约是 99 年底的事情吧。

当时查阅了许多资料,才把 decode 程序完成,同时写了一篇 JPEG 简易文档 。蒙网友厚爱,这么多年来不断转载,也总有人写信询问细节。今天收到一封不同的 email ,质疑我写的这篇非原创,乃一篇译文,却未在文中提及。

说来惭愧,当初的确查阅了不少英文资料,所以这篇文档也起源于其中一篇。怪当年年少无知,没有在文中注明。今日重读 CRYX's note about the JPEG decoding algorithm ,时隔多年,恍如隔世。当初动笔的时候,不知如何组织,照搬了这篇的结构,并且 copy 了其中的图表和公式。一开始只想粗略翻译一下,后来自己写写程序的时候发现许多细节不明之处,又查阅了许多别的资料(已经想不起来了,无法一一注明),陆续补充进文档。后来又有诸网友指教,经历了数次修订。

前几年修订的时候,也曾想过注明参考文献。皆因时间久远,无法找回原始资料,不能如愿。今天特更新注明,以尊重原作者。

ps. 如今的中文图书市场较之当年已大为改善,关于 jpeg 的专著已有翻译图书《JPEG2000图像压缩基础、标准和实践》出版,对其有兴趣者可以一读。

July 13, 2007

游戏中的货币

曾经写过一篇关于网络游戏中的货币经济系统的文章,由于我本人对经济学只是浅尝,读书不多,所知甚为有限,怕是贻笑大方了。但这不影响我对此继续抱有极大的兴趣。最近,又再思索那个困扰游戏设计者的问题:究竟如何用游戏规则来稳定虚拟货币。

首先,我认为现在的网络游戏中,货币并不完全等同于现实社会中的货币。现实社会中,货币只是一种能直接起交换手段或支付媒介作用的东西,它本身没有价值;货币的投放由央行控制。但在大多数网游中,货币本身有了实用价值,它可以从系统那买到各种补给品,或是可以直接增加虚拟角色的能力。而且货币本身也多由玩家自己的主动行为生产。

同时,在健康的虚拟经济环境中,虚拟货币也起到了现实货币同样的作用——作为支付媒介。

虚拟货币的多重职能,使得在架构虚拟社会的规则时,需要万般小心。而虚拟货币也往往因其多重职能,使得游戏中的市场调节能力减弱。现存的几乎所有网游中都没有设计出一个央行的角色,还真怪不得通货膨胀得不到有效的控制了。

那么,能否构建出一个更接近现实社会的经济模型呢?对于设计一个较之现存游戏更有趣好玩的经济系统,即使这不是唯一之道,但至少是一条值得尝试的路。今天在这里记录一些尚未深思熟虑的想法,权作自己的笔记。方家读过,请一笑了之。


部落战争 是我最近玩的一个网游,它的经济系统颇有特色。完全没有出现货币这个概念,全部是以物易物。这样的经济系统,在局部区域却也能健康发展,这对我颇有启发。现在暂时不展开讨论,读者诸君若有闲情值得一试。

恺撒系列近年已经出到第四版,除了最新的这一 3d 版外,旧作我都很喜欢。在这个系列中,突出了货币的作用。城市建设直接用金钱来进行,物资则用来维持建筑。除了初始资金外,金钱的来源主要为税收和贸易(后期有了铸币)。我在玩这个系列的游戏时,一直在考虑游戏中的钱的本质,到底它是什么?跟现实中的钱有何不同?既然把负债到一定程度当作游戏失败,那么游戏中赚钱或亏钱到底意味着什么?

再回头来看传统的网游,其实经济系统的平衡已经有许多游戏做的不错了。我们公司的梦幻西游就是一个代表。但正如前文所述,它并未在经济系统中提供一种等同于现实社会经济系统中货币的完全等价物。那么一个网络游戏,到底是否需要一种更像现实的经济系统呢?


云风的想法:

部落战争简化了资源设定,最终只有四种战略资源:黏土、木材、生铁、和粮食。这四种缺一不可,而且需要玩家调配其间的平衡。以物易物这件事上,仅存在 12 种定单,这简化了市场行为,不需要货币作为媒介也可以让玩家愉快的游戏。设计之妙,不在多多益善,而在减无可减。

玩家在恺撒中充当的是政府的角色。游戏中可以充分体验到,作为政府经营一个城市的辛酸苦辣。他的经济系统是完备的,玩家可以从中获得足够的乐趣。我们无须思考货币最初怎样诞生,它在游戏一开始就已经存在(而人类社会诞生之初,货币当并不存在)。玩家是以政府的视角维持这个经济系统的稳定发展,经济系统的崩坏则预示着整个社会系统的崩坏。至于修建奇迹(后来几个系列的大多数关卡的胜利条件),则是经济繁荣、物资充沛的一个表现。在这个游戏里,玩家扮演的政府持有的货币储量并不单是玩家的分数了。

MMORPG 类的网游并不能让每个玩家都扮演政府的角色,甚至也不应该过于鼓励玩家参于到集体活动中去。(关于这一点,我很赞同曾经看过的一篇名为魔兽世界之过的译文的观点,可惜那篇译文链接失效了,只留下我自己写过的一篇评论)。若是一个小的游戏世界,我们的确可以用梦幻西游类的方式来建立经济系统。设计人员去关心所谓货币的投放和回收问题。但是一旦游戏世界变大,如我所希望的那样:一个唯一的游戏世界,那么直觉告诉我,这条路不太行的通。

当年反对大世界的游戏设计的同事最为有力的观点之一就是:经济系统不宜全局控制,且容易崩坏。

今天我问自己的一个问题:现实社会中,央行怎样投放或回收通货?今天一起讨论的几个同事,没有学经济的,他们都未能给我满意的答案。我 google 了一下,又翻了下案头的《货币经济学》,似乎明白了一些道理。

这个貌似简单的问题,在网上居然搜索出许多可笑的答案。比如有人认为国家需要用钱了,就印一批出来用(比如做市政建设,修修三峡什么的,或是给公务员发工资)等等,借此把新的货币投入进市场。仔细思索就知道其荒谬之处。不过从这些回答中可窥见一斑:没想明白这个问题的人不只我一个 :D

我们的网游设计者,也正是在用此类现实中荒谬的手法解决经济系统的设计问题。farm 副本,刷金币等,都是在这种设计下的产物。我们的游戏设计者只好给每个怪物的金钱凋落(或任务奖励)设定一个八卦数字,然后通过怪物的刷新频率和难度做一个综合调整;还有像梦幻西游中的诸如师门任务等,再做金钱和物资的回收。现实中的荒谬不等于虚拟社会中的不合理。事实证明,这套方法在经过时间的磨合后,也可以运作的很好,我并不反对此类做法。但对于其理论本源的考虑还需要时日研究。

ps. 在网络游戏中,随意投放货币的这种行为,在现实中应该也有对应事物,这里就不举例了。我个人并不认为虚拟社会较之现实社会有特别的理论。本文不想多探讨这个话题。

在大世界的设定中,我们用距离和时间来隔离局部世界间的影响。在局部小世界中,我们需要一个更为可控的经济系统。然后在各个局部经济系统间则可以采用类似部落战争那样的以物易物的设计;又或者,像恺撒系列那样,城市间的物资交流依旧以货币作为媒介。只是由于不同经济体的存在,外汇系统必不可少。一个国家的货币要被世界承认,首先就是要自己的经济系统稳定。

最终,一个局部稳定的经济系统依旧是最重要的。

来看看现实经济系统中的通货问题。现实中的基础货币(我将其通俗的理解成所有金融结构以及民众所持有的所有货币)总量的增加,最终来源于商业银行向中央银行的借款,或是向系统之外,比如外国银行的借款。一般来说,流通货币的数量是小于基础货币的量的,所以商业银行的整体并不需要频繁向中央银行借款。而且他们也不希望如此。因为借款会引发中央银行对借款银行的资产管理(借款往往需要一些实物抵押)和其他做法进行监督。btw, 商业银行的个体之间,由于准备金(就是为了应付民众的提现的货币)的需求,倾向于相互之间拆借,这并不增加经济系统中的基础货币。

现存游戏中的虚拟货币(既虚拟经济系统中的基础货币)总量的增加,却不受“央行”的控制。打怪凋落金钱、NPC 回收垃圾、发放任务奖金这些,并无借贷和抵押的过程。那么,就让我们尝试加入这些:

让我们在游戏中设计一个金库。如果是一个玩家共同建设的游戏,可以在满足一定条件后被修建出来,若是一个非建设内游戏,则可以一开始预先设定好。这个我们看作是一个商业银行,货币将从这里流出。

当金库中(因为玩家行为)积累了一些黄金后,他用这些黄金做抵押,向一个玩家看不见的中央银行开始借贷。然后,金库中便同时有了黄金和货币。第一笔货币在系统中的出现的数量可以是人为设定的一个随意值,这个数值的大小意味这个经济系统中的金价。从玩家角度看,金库就是一个买卖黄金的地方。玩家群体可以从中得到第一笔钱。

当市场建立同时起来后,政府(游戏系统)保障任何以货币为媒介的交易。就好象魔兽世界中出现的拍卖所一样,玩家可以开始用手头的金钱交易物资了。(魔兽世界的拍卖所不太完备,因为它不能求够商品,是单纯的卖方市场,需要近一步完善。)

这里有一个很重要的要素,黄金必须在游戏中是玩家的消耗品。无论是作为日常消耗,还是奢侈品,有实用价值是必须的。这将使玩家在金库买卖黄金是有意义的。当然,我们可以用少许技术手段使得玩家更倾向于在市场做这种交易(最简单的方法是,金库只接收大单的黄金交易)。同时,金库里系统掌控的金价应该跟随市场做升降。当初期玩家向金库提供了足够的黄金后,玩家群体的货币拥有量便会在一定的数值稳定下来。由此发展下去,政府机构不一定只囤积黄金,还可以有更多的战略物资储备。央行也可以承认这些更多种类的战略物资做抵押。

由于玩家的背囊或仓库有限,他们必须把自己的物资不断交易出去,换成货币保存下来。而虚拟社会中的货币天然的是放在银行里的,并可以被统计和监控,虚拟社会的货币经济调控一定比现实社会容易很多。

July 06, 2007

唯一的游戏世界

今天写下上一篇 blog 之后,跟另一个组的同事讨论了我的这篇文章。他们的项目先是多服务器的架构,后来重构后改成了单服务器的设计。我们的讨论第一个出现的分歧就是在于对多服务器设计的复杂度会造成的影响。

讨论了一段时间后发现大家的对一些问题的定义从一开始就有区别。回头来大家换个角度讨论了一下,发现其实那些分歧都是不存在的。

讨论告一段落后,我浏览了一遍聊天记录,觉得有必要总结一下 :)

三年前,我跟公司一些策划同事有接连几天的争执:到底玩家是否需要一个宏大的虚拟世界?或是我们若实现这样一个虚拟世界,到底有没有价值?该怎样维护这样的一个大世界,让它不会受到一些暂时性设计 bug 的毁坏性影响?

可以说,三年前的争论是导致我成立独立团队做现在这样一个项目的重要起因之一。因为当时,我几乎得不到任何的支持,策划,程序,美术方面等等几乎都是不赞成的意见。

那个时候,我也的确找不到确凿的证据来说服大家。但今天,思路清晰了许多。

首先,提高单组游戏服务器的同时在线承载能力对给玩家带来更大乐趣并无太大积极意义。这是我一直都持有的观点。所以,我对公司已经完全购买的 Bigworld 游戏引擎不以为然。这个引擎最大的优势在于它的服务器组负载均衡做的非常不错,可以让更多的玩家互动。btw, bigworld 在技术上是非常不错的,代码质量就我读过的部分来看,也是颇佳。

如同现实中的人的社交活动一样,人们不需要太多人的完全交互,也不希望独身一人。人在一个合适规模的团体中生活是最舒适的。一味的提高单组服务器的承载能力,有如扩大虚拟世界中人同时能参与的社交活动的规模。可就算在虚拟世界中开奥运会,也没有人去参加所有的项目。也不会让所有的项目在一个体育场去开。

所以我不关心一个逻辑服务器是由多台机器还是单台机器构成的,那些是技术细节。游戏的设计本身就应该避免很高承载力的需求。

但是,我们需要保留所有玩家在一个整体的虚拟世界中相互交流的可能性。这会衍生出很多给玩家带来更多乐趣的新玩法。这些跟简单加大交互数量的所谓“更多人的国战”是有区别的。

设计所谓跨服务器的玩法,也应该和单一服务器内部实现的玩法有所区别。就如现实世界中,你或许可以跟银河系那一头的外星人交换信息,但是绝对不可能发生实时的互动。光速的极限在那儿摆着呢。

我们设计的跨服务器的玩法,都可只基于一个原语:即对象的传递(而不是类似对象的远程控制之类的东西)。我们需要唯一解决的技术问题是:如何安全的把对象传递到另一个世界中,而不出现丢失或复制。时间和效率是很次要的东西,这一点跟服务器内的交互设计截然不同。

一个全局的数据库是没有必要的,各个游戏世界应该是对等的,没有一个特权世界。信息的即时同步永远是没有必要的。这跟现实世界中,永远有光速的屏障一样。

比如一个帮会系统,成员分布在不同的服务器中。那么查询到当前所有帮会成员名单这种需求就是没有意义的。最多你可以查到当前世界中的名单,但是你对另外世界的成员名单的了解只能是过期的信息。

玩家角色在服务器之间迁徙也是这样,我设想的最简单合理的设计是:角色必须光着身子被传送到另一个世界(好似终结者)。也就是角色本身的等级技能这些被复制(或传送)过去,而物品则需要通过游戏中的物流来输送。

角色被传送后,可能需要一段时间才能到达,但玩家可以操作他在原有世界中继续做一些有限的事情。一旦送达后,踢出旧世界,在新世界登陆即可。我们甚至可以不允许在新世界中被传回,只需要把新世界中得到的经验等人物属性转换为物品邮寄回来即可。你在那个世界中的你可以看成你在新世界中的一个副本。

游戏的局部世界和局部世界之间只要设计出逻辑上的距离,距离制造了时间差。由于对象传递是一个原语,一切服务器间的交流都依赖这个原语;这就好比现实世界的光速屏障一样;可以有效隔绝统一世界中各个局部间的矛盾。无论这些矛盾是由于部分玩家在局部利用一个 bug 引起,还是大家发展时间长短的不一致引起的。

我希望在我们的新游戏中,既可以让玩家个体做个人的奋斗,又可以让团体在一个唯一的大世界中攻城略地、争夺资源、建立商业网络…… 我想这些都会是激动人心的东西。我知道很多网游都想过这些试着做这些,却几乎没有看到谁成功的给玩家带来更多的乐趣。那是否这次实现会略有不同呢?

July 05, 2007

游戏服务器组间的通讯

网络游戏世界的构建有越来越大的趋势,游戏设计者希望更多的人可以发生互动。技术人员的工作就是满足这些越来越 BT 的需求。

我们目前这个项目由于是自己主导设计,而我本人又是技术方案的设计者。所以,技术解决不了的问题就不能乱发牢骚了。作为游戏设计者,我希望整个游戏世界的参于者可以在一个唯一的大世界中生存,他们永远有发生互动的可能。注意这里只是保留这种可能性,实际上,即使是现实社会,每个人的社交圈子都不大。即使是千军万马的战场上,无论是将军还是士兵,都不需要直接跟太多人互动。

我们的游戏的技术解决方案仍旧是将游戏大世界分成若干独立服务器组,人为的将人群切分成更小的独立单位。这里,技术上需要解决的是:服务器组间可以灵活的交换数据。

最近几天正是在设计服务器组间的通讯方案,初步的想法是在每个服务器组中安放一个独立服务进程,称之为“邮局”。各个服务器组中邮局之间可以相互发现并交换数据。

服务器组内部的各个进程通过向邮局投递或收取邮件来跟其他服务器组通讯。这个邮局提供类似 POP3/SMTP 的服务。实际上,我在通讯协议设计上,几乎照搬了 POP3/SMTP 。不过,在原有协议上加了一个强制的回执。这个对内部交互通讯的可靠性非常重要。

有了这样的设计,各种玩法的实现就会比较容易。比如对于玩家的迁徙,只需要在每个服务器组内设置一个“移民局”。玩家迁徙请求发出时,系统发出邮件到对方的“移民局”,把玩家数据 copy 过去。等到回执后,再要求玩家在新服务器组上登陆。同样,玩家物品的转移和商业系统也可以依靠专门的“联邦快递”进行。

邮局系统可以以单个队列来完成其它模块的请求,用时间来换取低负载。只要不设计出高度要求交互的跨服务器组玩法,这些应该是足够满足要求的。

有了邮局系统,我们甚至不再需要庞大的全局数据库。每个服务器组保留自己独立的数据库。只在发生交互的时候相互用邮件交换数据。全局的统计也可以由互发邮件来完成。

July 01, 2007

更健壮的 C++ 对象生命期管理

以下的这个 C++ 技巧是前段时间一个同事介绍给我的,而他是从 fmod 中看来。当时听过后没怎么在意,主要是因为这两年对 C++ 的奇技淫巧兴趣不大了。今天跟另一同事讨论一些设计问题时,突然觉得似乎在某些地方还有点用途,就向人介绍了一番。讲完了后觉得其实还是有点意思,不妨写在 blog 上。

问题的由来是这样的:音频播放的模块中比较难处理的一个问题是,波形(wave sample)数据对象的生命期管理问题。因为你拿到一个对象后,很可能只对它做一个播放(play)的操作,然后就不会再理会它了。但是这个对象又不能立刻被释放掉。因为声卡还在处理这些数据呢。我们往往希望在声音停止后,自动销毁掉这个对象。

另一些时候,我们还需要对正在播放的声音做更细致的控制。尤其在实现 3d 音效,或是做类似多普勒效应的声音效果的时候。

C++ 中传统的方法是用智能指针的方式来管理声音对象,但这依赖语言本身的一些特性。fmod 提供了诸多语言的接口,它在为 C++ 提供接口的时候利用了一个更为巧妙的方法。

声音的类工厂并不需要生产出一个真正的对象指针,而是返回一个唯一 ID 。但是在语法层面上看,它却是一个对象指针。也就是说,如果你用调试器去看这个指针的值,他很有可能是 1,2,3,4 这样的数字。

如果这个类不提供任何虚方法,也没有直接可以访问的成员变量的话,对这个指针做任何成员函数调用其实都是合法的。因为 C++ 的普通成员函数调用仅仅只是把对象指针做为一个特殊的叫做 this 的参数传入函数而已。比如这样的代码是完全可以正常运行的:

class A { public: void test() { printf("%d",(int)this); } }; int main() { ((A*)1)->test(); return 0; }

那么库的实现只需要在每个成员函数的开始,利用一张 hash 表,把 this 表示的 id 转换成内部真正的对象指针即可。如果这个 id 对应的对象已经被销毁,则可以安全的退出函数调用。

这个技巧提高了库的健壮性,其代价是每次成员函数调用都需要多一次 hash 表查询操作。如果想优化一下性能的话,不妨 cache 住最近访问过的对象。