« December 2005 | Main | February 2006 »

January 31, 2006

关于读研这个事

这两天过年在家陪父母,其他也没什么事,老同学大多有自己的小家不是每年都回武汉了。U 盘带回来的一些代码也不太想看,家里机器除了个 mingw 也没别的编译器,要说写程序还真没啥兴趣了。那么就只好上网到处逛逛了。

今天看到一个 blog ,跟主人在上海有一面之缘吧,看照片特眼熟。文章是这么一篇 http://www.blogcn.com/User5/omale/blog/28119593.html 里面写了好多东西,我印象深刻的是关于读研这一段。(注:这篇文章是转载,并非博主本人)

我是本科学历,还差点辍学没读完,当初学校的老师也流露过让我留下来读研的想法。不过看我学习太差,还在大四闹脾气,整出个退学那挡子事,也就没好明说。说了我也是打死不留在学校的。我现在的领导倒是硕士毕业,每每出去招聘前,都要叮嘱,不准歧视硕士,我们公司不能有学历歧视。结果呢,硕士博士,从结果看还是被我们“鄙视”了。

我这个背景应该没什么权利评价读研这件事吧?以为然也者不必点开看下面了。

总的来说,我是很看不起现在国内的研究生教育的(仅指计算机这一科)。

本该在本科学好的东西全放在硕士阶段去学了,还不一定学的好。传道受业的老师,居然被学生们亲昵的称呼为老板,跟狭隘的经济搭上关系。老师居然跟学生发起工资来,而学生居然还要感谢老师给予的这种社会实践机会(听来的许多学生语)。研究这档子事,就变成了一种糊口或者换取学历的劳动。而学校里的大部分软件项目,在我有限的了解看来,几乎都没有它真正的学术价值,甚至没有实用价值。我们公司里做软件,还精挑细选开发人员呢。就那些个干了几年,积累过经验的大部分研究生我们都看不上。真难以想象靠这样一批良莠不齐的缺少坚实技术基础的业余选手,一个项目怎么可能被优秀的完成。那该需要项目领导者多大的才能啊。

其结果是,工程项目一年复一年的低水平重复;学术研究项目,不停的跟在所谓的世界前沿做一些无关痛痒的改进和校验。而参与项目的学生们,只是把 coding 的熟练度增加了一点。

看过网上一个博士找工作的笑话,而我这里不是笑话。多少次面试完一个毕业生,都感叹,如果他是一个本科生就好了,虽然还达不到要求但是有培养的前途。

光从软件公司来看,以我做了 4 年的 HR 的经验,我想说,做一个程序员,超过本科的学历只能给你的毕业求职过程减分。不要误解这句话,如果你本身优秀,这个减分是微不足道的;你不优秀的话,过高的学历绝对是一个累赘。至于更高的待遇,公司是为你的能力来付薪,而不是你的学历;只是在短暂的招聘过程中,无法确定一个人的时候,才会信任国家的教育允诺一个稍微高一些的起薪。在以后的加薪中,干的同样出色的话,学历越低的人越有机会;高学历会被人认为高能力是应该的。

话说回来,我自己还是很希望有一天回到学校潜心做点研究。做什么事还真是要看环境的,学校就是个学习和研究的地方嘛。不为文凭,我已经不需要向别人来证明自己;不为钱,以我的需求和现在的积蓄,简简单单过完一辈子也够了;也不需考虑所谓事业的发展,这个社会不会只因为有我变的更好,也不会只因为没我变的更糟糕。我可以静心做点事情,尽量的让周围的人和事变的更好吧。

ps. 本科学历还是很有用的 :) 如果只有个中学文凭,至少在技术上,第一感觉会给人不太好的印象 :) 如果是求职,你需要花更多时间和精力以及机会去证明自己。本科辍学就更没必要了,就算瞧不起中国的大学教育,本科四年都熬不过去,还指望做什么大事?正常的上学,本科毕业出来也才二十出头,没毕业出来都可以算是童工了,太早出来混社会,对人生也没太大帮助 IMHO

January 26, 2006

明天回家过年

好几年没有在大年初一之前回家了。今年元旦没给自己放假,明天提前放假回家。幸亏先把票买好了啊。刚才查了一下,明天回武汉的连头等舱的票都没了。春运真叫个紧张啊,以往都是年初一初二买票回家不觉得,还能打个折什么的。

January 24, 2006

睡眠瘫痪症

第一次碰到鬼压床大约是上小学放暑假的时候,早上在地板上昏睡不想起来,直到床外的阳光射到眼睛,拼命的想睁开眼起来却办不到。后来知道了这个医学上的学名叫做<a href="http://www.google.com/search?client=opera&rls=en&q=%E7%9D%A1%E7%9C%A0%E7%98%AB%E7%97%AA%E7%97%87&sourceid=opera&ie=utf-8&oe=utf-8">睡眠瘫痪症</a>。我不知道和我自己经历的是否吻合,因为我在发生的时候,除了意识比较清醒,并不能感受到周围的信息。

自从我没有朝九晚五的压力后,这种现象就越来越多。一般表现就是,早上醒过来头很痛,坐起来后无论怎么活动,头依然是痛的,有种没睡醒的感觉;实际上我的身体并没有动,一切只是幻觉,偶尔还会有身边的人走过来交谈等幻觉。这几年除了出差在外,我几乎每天都是 12 点起床(晚上 3,4 点入睡),据说睡眠瘫痪证是因为过度疲劳或者作息不规律等引起的。我自己看来,虽然我晚睡晚起,但作息还是很有规律的。而且也从来不让自己累着,困了自然就去休息了,平时也坚持锻炼身体,身体状况不错。只有去年有一段时间,企图把自己的作息时间调整一下,最后弄的生物钟紊乱,每天睡不了几个小时,失眠,精神不佳。

如今多的时候一周能有一两次这种现象发作吧,少的时候几个月一次。跟网上看来的别人的情况不一样,或许是我习惯了,每当出现这种情况时,并没有什么恐惧感,而且能够在几秒内清晰的意识到自己并没有醒来,可以准确的分辨出哪些是幻觉来。

这个时候,并不能感觉的周围的情况,但是会有感觉到周围情况的幻觉。跟作梦是不一样的,也谈不上噩梦。只是幻觉自己坐起来但实际躺着并没有醒,或者知道附近有人,但无法跟他们传递信息。现在遇到这种情况都会很镇定,不过想醒过来却是要颇费一番工夫的。以前我都期望这个时候能有人碰我一下,或者有电话打进来,但是时间长了,知道这些发生的几率太小,只能靠自己。

曾经有人告诉我,睡眠瘫痪症是人快速进入深度睡眠后,意识提前清醒过来,而肢体的肌肉仍停留在低张力状态不受控制造成的。应该试图去操纵最容易控制的肌肉,比如小指,电视剧里那些瘫痪的人醒过来都是指头最先有的知觉。而我的经验是,动指头实在是很困难,比较有效的是动一下脖子,扭一下头。或许我潜意识里觉得脖子离大脑最近吧,神经传播距离最短。在这种半梦半醒的状态企图扭脖子是一件非常痛苦的事情,非常的酸,不过如果真的能动一丁点,就自然醒了,酸痛感就会消失。不过这个时候要避免自己再次入睡,否则头会更痛。

我自己总结的经验是不能太贪睡,如果一天睡眠超过10小时就很容易产生问题。也不能在一些不太舒适的环境睡觉,应该待自己好一点 :) 适当的运动是很重要的,身体运动后的劳累感,通过睡眠很容易改善,也会睡的很舒服。不过我自己为什么睡眠质量老是比别人差一直没有找到原因。

不懂比懂更重要

今天看见一句话觉得很有道理:高手之所以为高手并不是比菜鸟们懂的多,而是不懂的比他们多。

January 22, 2006

Windows 下最小的汉字点阵字摸

以前一直以为 Windows 下汉字点阵最小是 12px 的,依稀记得有朋友提过,其实还有更小的汉字字模。今天刻意想找,发现原来 MingLiu, PMingLiu 里提供了 11px 的汉字。因为字之间需要留白,实际字模是 10*10 大小的。

日文提供更小的汉字字模,MS UI Gothic 提供 10px 的点阵字体。

ps. 以前一直觉得 Opera 显示小字体的汉字怪怪的,有些网页看的不舒服。我用的英文版的 Opera 8.51 。今天找了下菜单,Tools - Prefences (Ctrl-F12) - Fonts 原来默认最小的字体(Mininum font size) 是 6px 的,改成 11px 后,在把默认中文字体设置成 MingLiu (International Fonts...) ,舒服多了 :)

January 20, 2006

基于TCP数据流的压缩

这两天研究了一下 lzw 压缩算法,据说它专利已经过期了,那么应该可以随便用了吧。

这种基于字典的压缩算法,一个很大的优势就是,如果数据流中经常出现同一个词,那么会被极大的压缩掉。重复性的信息在网络游戏中经常碰到,不过是基于包和包之间的,而不是同一个包之内的。游戏又跟别的应用不太一样,我们往往希望更快的把数据交到对方,减少网络的延迟,所以大多数情况下,我们不会积攒很多数据一起发送。所以,不能简单的以每个数据包为单位调用压缩器。

其实最简单的方案是,压缩器的上下文被保留起来,也就是说每次压缩完毕后不重置字典,这样,重复的信息会被压缩的越来越短。我花了一晚上时间实现了个 lzw 算法做实验,因为lzw要求双方有相同的初始化字典,一般是所有可能出现的字符集。如果以 byte 为单位压缩,这个字符集就是 256 个。而压缩后的码长至少就是 9bit 起。为了方便,我在字符集中加了一个包结束符。这样,分包的时候就不需要再保存包的长度,而以这个结束符结尾就可以了。这样在解码处理时就非常方便,碰到结束符就知道一个包结束了。

为了优化,我们也大可不按 8bit 为单位压缩,比如可以用 4bit 分段,编码的码长就可以从 5 bit 开始。实际测试的效果也比较好。

写好程序后,我用 "Hello World" 这个字符串做了测试。
字符串一共是 11 字节,加上结束符信息大约需要 12 ~13字节的信息方能描述。
重复压缩 25 次结果如下:

04 99 62 58 35 4f 80 72 f0 11 2e 02 02
d1 74 59 d9 d6 3d 5f 08 01
23 65 99 1a e7 81 96 08
6d 89 c5 e9 5a 43
f6 8b a3 66 2c
bd cb 49 a6 a2 19 44
42 18 d0 c8 02
c9 5f 71 87 84 00
4d a2 ee 28 02
d2 5c 90 07
d6 25 54 04
d9 67 15 02
5c 2a 0b
5f 6c 14
61 26 0f
e3 2d 04
65 2f
67 16
68 1e
e9 28
6a 11
6b 08
6c
6c
6c

我们可以见到,第一次进入压缩器后,输出是 13 字节,比原有的数据稍长一点,但是随着次数的增加,越来越短。最后只用一个字节就可以表示这个串了

这次这个程序,每个编码器需要占用一块固定大小的内存,可以调整。如果按 32k 计的话,每个连接需要 64k 内存。那么 1000 个连接就是 64M ,算是一个不小的开销。我不知道在千M网上还有没有意义,因为网络流量增大后,如果网卡承受的起的话,给 CPU 的负担无非是每次发送从用户态向内核态拷贝数据的时间,这个速度非常的快,远远小于编码器的开销。

除了 lzw , 我还考虑了动态 huffman 算法,熵编码其实也不错,不过动态 huffman 树调整算法很复杂,时间开销更大,相比较还是 lzw 更具有实时操作的可行性了。

不过引擎提供这么一个特性还是满好玩的,纯属娱乐吧。

ps. 晚上拿大话西游的包测试了一下,2504 秒内,client 共收到 32903 个包。总共收到 943888 字节,压缩后 700508 字节。大约压缩到原始数据的 74.2%

我审查了一下自己算法的实现,感觉还有许多改进的余地,打算周末仔细改造一下。

January 16, 2006

貌似合理的网络包协议

最近有个小项目,很快的开始,似乎也能很快的完工。就一个不大不小的游戏,2d 的,图象引擎是成熟的,然后我就这这段日子对 lua 的热情,用 lua 把原来写的 C++ 图象引擎做了个封装。用起来感觉良好,UI 部分封装的也不错。游戏逻辑用 lua 驱动貌似很方便。一度幻想着哪天把它开源出去,没准可以成为 lua 开发 2d 游戏的准标准。想想可能性不大,纯当意淫了。

小项目比较能锻炼队伍,所以我做为基本封装就跟新同事上课了。看着别人做程序心里痒痒的,做不出来急急的。恨不得什么都自己代劳。

其中一个同事的工作安排是网络协议的封装,争取用 lua 封装的好用一些。

老实说,我对网络编程的实战经验非常有限,总共不超过 1 万行代码。写的可以运行的可以称之为程序的东西不超过 5 个。其中一部分是基于 UDP 协议的,而这次这个小项目打算用 TCP 协议。我不禁想起大话的通讯协议,首先一个字节的 type 和接下来两个字节的数据包长度信息。

当初,我对这个协议颇有微辞。因为 tcp 是一个流协议,如果统一用数据包长度来分包,再由 type 来 dispatch 逻辑的话,最合理的应该是先把长度信息放在前面,让最底层的代码把数据流分割成数据包,然后就可以把整个数据包交给网络无关的层次去处理。

这次设计协议的时候,希望可以用 lua 脚本来描述每个协议,用 C++ 代码将网络数据包解析成 lua 的一个 table,这样,lua 就可以很方便的处理网络传送过来的数据了。

非阻塞模式下,socket api 处理 TCP 协议,只有在读单个字节时才能看成一个原子操作。这样当我们的数据包长度大于一个字节时,都不能默认它是一次可以读到的。所以,处理网络数据流的过程一般用一个状态机来实现。这个状态机可以实现的很简单,长度+类型+内容这种格式,我们可以看成状态机的三个状态。 程序的主逻辑里,我们只需要不停的调用状态机的处理过程,就可以完成对网络数据的处理。

我按这个思路设计程序的时候却发现,长度完全是个纯粹的冗余信息。因为,如果我们用脚本定义好了每种协议的格式,那么长度已经可以根据协议的 type 推导出来。大部分的协议格式是由定长的元素构成,最终它本身也是定长的。有些元素是不定长的,例如 string 和 array 。那么整个的长度还是可以根据上下文获得。至于处理后一种,无非是增加了状态机处理程序的一点点难度而已。

这样,我不再在数据包头放上包的长度。

January 13, 2006

程序员一年究竟能有多少代码产量?

前两天在厦门做了个即兴的演讲,我随口说了个数字说我现在大约一年的代码产量是20万行。当时雷军表示不信,下来我们讨论了一下20万行的可能性,雷军的观点是,要保持年产20万行代码,必须每天写 800 多行代码,因为要留出思考的时间,所以代码日产量是远高于这个数字的。所以不太可能。

我今天统计了一下2005年 8 月份到年底的代码量,发现轻易超过了8 万行的产量。8月开始我和同事做的一个主要的项目,目前的统计数字是 9 万余行代码(几乎是用 editplus 手写,无 copy-paste 和机器自动生成代码,甚至都没有用 VC IDE 里的自动完成)这个项目目前留下来的代码是 9 月底开始的,在此之前两个月的东西我们全部重构了,懒的把老版本从仓库里拿出来统计,就算 2 万行吧。那么我个人的代码产量就是 5 万余。

前段时间写了个虚拟机和一个脚本编译器外加测试代码,统计下来是 6000 余行,在此之前用 C 做了一些虚拟机的实现,大约 1500 行。

前段时间要新开一个 2d 游戏的项目,我把 2d engine 用 lua 重新封装了一次,给其他的程序员用。这部分代码 2500 余行。另外作为演示,写了大约 1000 行的 lua 脚本。

另外一些零散写着玩儿的小东西 20 余个,平均每个有代码 500 行左右,按 1 万行计算。

以上没有计算手写 jamfile 和编写 jam 脚本的代码量,还有对以前一些老项目的修补。我是不喜欢写注释的,所有代码中,注释和空白行占的比例是 10% 。

这小半年,我另外做了许多策划以及管理的工作,代码效率大不如上半年,所以我认为年产 20 万行的数据应该没有浮夸。

当时雷军跟我算的时候说,要保证年产 20 万行代码,就要保证每天写 800 多行。我自己除了一下,20w / 365 只有 500 多,这才恍然大悟,原来说的是工作日啊 :) 偶现在是把写程序当作乐趣,所以反而周末的产量比较高了。工作日的杂事很多,很多时间不能去写程序。

记得以前测算过,一般一天下来,接近 2 千有效代码的速度是跟的上的。平时写点小东西玩的话,一个连续的时间片,大约都是四五百行的数量。

当然一个好程序员绝对不应该用代码的绝对行数来衡量水平,甚至我认为越好的程序员应该代码行的产量越低。这两年我的代码产量就明显低于前几年了 :D 那么以后,我应该朝年产10万行代码奋斗了。

January 10, 2006

准备动身去厦门

今天动身去厦门,参加游戏产业年会的民族游戏研发论坛。不过现在工作很忙,明天发言完了晚上还得赶回来工作。

January 05, 2006

动态加载资源

如今很多游戏engine宣称自己支持动态加载地图,也就是说可以作到跨地图时的零读取时间。听起来很高深的技术,实际不难实现,当然我们在大话西游和梦幻西游中早已经实现了。最近我正在考虑更加通用的解决方案。

先说说基本的思路。也就是我们需要把地图数据切割成小块,让每一块的数据读取解析量并不太大。然后,就可以根据玩家所在坐标读取最小数据量的数据。当玩家移动的时候,利用一些机器闲置时间去读周围的场景,或者进一步可以根据玩家移动方向把前方数据预读的优先级别调高。

其实,这个方案可以被推广到所有的游戏资源。其实,我们可以给每个资源文件都放置一个通用的数据域存放一些建议关联文件列表。如果是地图块的话,它的建议依赖文件就是它周围的地图块,或者是人为设置过跳转点的目标块。

而模型文件则可以把它依赖的贴图,骨骼文件做为建议依赖资源。地图场景文件可以建议依赖一些摆放在上面的模型文件。

有了这些依赖关系,我们在读取某个指定资源文件后,可以用一个独立线程预先加载一些可能下一步要加载数据,也可以由逻辑层主动通知一些预测信息。例如我向东向移动,可以通知 I/O 线程,东边的相关资源优先级别高一些,I/O 线程则去调整预读队列的次序。

I/O 线程的设计是比较复杂的,我的最初想法是建立一个大的 cache, 以内存页对齐(IA32 下是 4K)。如果数据被预读了,主线程索要数据的时候,用共享内存的方式,将 cache 中的数据块交换出去。

经过和同事的讨论,一个做 OS 的同事提示我,设计可以更简洁一些。因为现在 OS 对 file I/O 的 cache 机制都非常完善了,预读只需要去真的读一下文件即可,不需要再去自己做 cache,自己做 cache 只能节省一些数据 copy 的操作,但是其逻辑的复杂程度还有页表操作的负担不一定划算,所以我们只用专心做好预读逻辑即可。

这样的动态加载的资源管理模块,完全可以做的对主线程完全透明。我们的主线程只需要按阻塞模式去读取文件就够了。

但是,如果读取文件还伴随着耗时的解码过程。比如解 jpeg,解压缩,解密。那么我们最好自己做 cache 了。

January 03, 2006

安装字体

以前做游戏想用隶书,但是并非每台机器上都装有这个字体,所以有时候需要给用户提供一个。在自己软件的路径下放上字体文件,直接调用 CreateFont 这个 API 是不认的。

这种情况下可以使用 AddFontResource ,然后调用
PostMessage(HWND_BROADCAST,WM_FONTCHANGE,0,0); 就可以了。
当不用这个字体时再调用 RemoveFontResource 卸掉。

January 01, 2006

在中学100强中看到了母校的名字

<a href="http://www.google.com/search?client=opera&rls=en&q=%E4%B8%AD%E5%AD%A6100%E5%BC%BA&sourceid=opera&ie=utf-8&oe=utf-8">中学100强</a>

这是则老消息了,不过今天才看到。我的母校居然排在 17 :D 想起很多中学时代的事情,很美好,也能牵动几根记忆深处的神经。2006, 转眼就毕业十年了,随着同学聚会的频率越来越小,到会人数越来越少。同学一个个的有了自己的家,分布在地球的区域也越来越宽广,怕是以后的联系也会逐渐减少了。

大家都还好吗?

ps. 最近连着收到几个老同学的电话,就只是单单问问好,觉得真是很难得了。

向 lua 虚拟机传递信息

当程序逻辑交给脚本跑了以后,C/C++ 层就只需要把必要的输入信息传入虚拟机就够了。当然,我们也需要一个高效的传递方法。

以向 lua 虚拟机传递鼠标坐标信息为例,我们容易想到的方法是,定义一个 C 函数 get_mouse_pos 。当 lua 脚本中需要取得鼠标坐标的时候,就可以调用这个函数。

但这并不是一个好方法,因为每次获取鼠标坐标,都需要在虚拟机和 native code 间做一次切换。我们应该寻求更高效的方案。

编写脚本的人可以只获取一次鼠标坐标,然后把数据放进一组全局变量。在一个运行片内,不再调用 get_mouse_pos 函数,而是通过访问全局变量来得到鼠标的位置。

从这个方案,我们可以引申开,其实这个全局变量可以由 C 程序主动设置,在 native code 的运行片中,Windows 消息处理完后,直接讲鼠标信息设入 lua 虚拟机。代码类似这样的。

lua_pushstring(L,"MOUSE_X");
lua_pushnumber(L,mouse_x);
lua_settable(L,LUA_GLOBALSINDEX);

lua_pushstring(L,"MOUSE_Y");
lua_pushnumber(L,mouse_y);
lua_settable(L,LUA_GLOBALSINDEX);

但是这里,依旧存在一个效率问题,那就是 lua_pushstring 。我们知道,lua 虚拟机中,每次产生一个 string ,都需要查对 string 在虚拟机中是否存在相同的拷贝,如果存在,就直接引用已有的;如果不存在,则产生一份新的拷贝。

这里,MOUSE_X 和 MOUSE_Y 两个 string 除了第一次运行,以后都是存在于 lua 虚拟机中的,虽然不会产生新的 string,但查找和比较字符串依然会消耗一定的时间。下面,我们来优化这个 lua_pushstring 操作。

我们可以在程序开始阶段,创建出这两个 string ,并且在 C 中保留引用。
lua_pushstring(L,"MOUSE_X");
_mouse_x_ref=lua_ref(L,-1);
lua_pushstring(L,"MOUSE_Y");
_mouse_y_ref=lua_ref(L,-1);

那么,以后运行时就不需要再做 lua_pushstring 操作了,而改成相对较快的 lua_getref 操作。

lua_getref(L,_mouse_x_ref);
lua_pushnumber(L,mouse_x);
lua_settable(L,LUA_GLOBALSINDEX);

lua_getref(L,_mouse_y_ref);
lua_pushnumber(L,mouse_y);
lua_settable(L,LUA_GLOBALSINDEX);

lua_getref 之所以相对快一些,是因为 lua 对数字做 key 的 table 操作有优化处理,直接变成一次指针操作。而 ref 就是记在一张全局表中的。而且 lua_getref 不需要 lua_pushstring 做过的 strcmp 操作。

那么这个方法还没有优化余地呢?答案还是有。

我们其实可以写一个 lua 程序,放在一个单独的文件(mouse.lua)中,程序很短:

return funtion(mx,my) MOUSE_X,MOUSE_Y=mx,my end

我们在程序启动的时候运行
lua_dofile(L,"mouse.lua");
_mouse_set_ref=lua_ref(L,-1);

那么,在设置鼠标坐标的时候就可以简单的做如下操作:
lua_getref(L,_mouse_set_ref);
lua_pushnumber(L,mouse_x);
lua_pushnumber(L,mouse_y);
lua_call(L,2,0);

这个方案只需要保留一个函数的 ref ,并且把设置的工作交给了虚拟机中的伪指令。单从这个例子(仅仅 MOUSE_X,MOUSE_Y两个需要传递的信息)来看,不能说明后者的效率更高一些,毕竟 lua_call 也有额外的消耗。但是,最后一个方案更加灵活,对于native code 向虚拟机更多数据的交换采用这种方案更加简洁。

ps. lua_ref 的东西,最后要记得调用 lua_unref 解引用。