有趣的进化心理学

最近在读《进化心理学》,进化心理学是心理学中非常有意思的一个领域,近些年来发展非常快,成果也非常多。通过进化的观点来为人类及其他物种的许多行为和心理偏好寻找解释,是一个非常有趣的解读方式。
通俗的说,所有的物种包括人类的很大一部分行为和心理机制都是在自然选择的过程中被逐步筛选出来的——筛选的原则是这种行为或心理机制是否曾经为该物种的生存或者繁殖提供了某种好处,从而使得持有该基因的个体更容易获得选择优势,并且在繁殖的过程中将这种行为/心理机制的基因传递给了后代。这样,最终延续下来的个体大都保留了该行为方式/心理机制。
简而言之,进化心理学的核心观点可以简述为:自然选择的压力持续作用,种群中的部分个体有某种可由基因操控的行为或者心理机制使其在自然选择的压力下拥有生存或繁殖上的优势,繁殖行为将该基因及其决定的行为、心理机制和可能存在的副产品传递给后代。
人类对蛇有天生的恐惧感,这源于我们祖先在进化过程中生命不断受到蛇的威胁,获得了对蛇的恐惧感的人更好的适应了有蛇存在的环境,从而有更高的几率活下来。但是由于进化机制只能在非常大的时间尺度上起作用——因此现代社会的汽车出现才刚刚100多年的时间,虽然每年丧生于车轮下的人数远远多于丧生于蛇的攻击的人数,但是在短短的几代人的时间里,人类还未能对汽车还未能建立起相应的心理机制。(事实上,如果人类社会的秩序和法律持续发展,遵守秩序和法规可以带来安全性和生存优势的话,也许未来社会的中人类会进化出遵守秩序和法规的心理机制)
人类对食物的偏好中也有进化的痕迹存在:人类喜欢香料很可能是因为香料能够杀死食物中的微生物或者抑制微生物生长,从而阻止毒素生成。人类喜欢喝酒则很可能是由于灵长类动物在历史上有2400万年的食用水果的历史——水果散发出来的“乙醇香味”是其成熟的极佳线索,灵长类生物在食用水果的同时,也获得了对酒精的偏好——这种偏好未必是一种有利于生存的适应行为,而很可能是灵长类祖先过度沉溺于水果类食物而带来的适应不良(进化的副产品)。孕妇在怀孕的头三个月中,通常会有妊娠反应——对某些特定食物高度敏感,并伴有呕吐反应,心理学家找到了强大的证据证明妊娠反应是一种适应机制,它能够阻止孕妇摄入或吸收不利于胎儿发育的有害毒素。
人类对美的一般标准的判断是近似的,审美标准是在百万年的进化中逐渐形成的,许多美的特征——丰满的嘴唇,光洁的皮肤,亮泽的头发,对称的面孔,恰到好处的肌肉和匀称的体型等外貌特征往往说明了身体健康,没有寄生虫或毒素,高的生育力和高繁殖价值。这些外貌上的线索提供了女性繁殖价值的最有效的标准,远古男性从而得以进化出对拥有这些线索的女性的偏好。相反,如果对这些高生育力和高繁殖价值的品质不加重视,反而偏好皮松肉弛,头发灰白的女性,那么该类男子很可能繁殖不出什么后代,其家族血脉也终将灭亡。
心理学家Judith Langlois和她的同事们关于婴儿对面孔的反应的研究为前述观点提供了证据:
先呈现白人女性或黑人女性面孔的彩色立体图像,让成人评估她们的性魅力。然后给2-3个月以及6-8个月大的婴儿呈现这些性魅力程度不同的面孔。不管是较小的婴儿还是较大的婴儿都对更有魅力的面孔注视得更久,这表明美的标准显然在生命早期就已经出现了。该证据对一般认为性魅力标准是在当前文化模式中逐渐形成的观点提出了挑战。
美的标准存在跨文化性,心理学家要求不同种族的人评价亚洲,西班牙,黑人以及白人女性的照片中有吸引力的面孔时,发现在评判一个人是否好看时存在着惊人的一致性。
最新的技术也为进化心理学提供了新证据,心理学家给异性恋男性被试提供四组吸引力不同的面孔——性魅力事先评判为:迷人的女性、普通女性、迷人的男性和普通男性,当使用核磁共振技术扫描男性的大脑时,发现男性观看迷人的女性时,大脑的伏隔核区域(nucleus accumbens area)显得非常活跃——众所周知,该区域是一个基本的奖赏回路,已被证明是脑部的快乐中枢。而男性观看普通女性和任何男性面孔的时候,该区域都不会被激活。简而言之,男性在注视迷人的女性面孔时会获得大脑中的奖励,这可以解释为什么大多男生都那么爱看美女: )
人类在择偶和抚育后代的行为也有进化的痕迹。比如,我们知道人类在99%的进化历史中都过着狩猎-采集的生活,所以可以预测女性进化而来的择偶偏好中,应该包含成功狩猎所必需的特定品质,比如运动能力,良好的手眼协调能力,长途狩猎所需的耐力,显示力量的高大身材等等。
而亲代投资理论认为两性中对后代投入更多资源的一方(通常是雌性,但是摩门蟋蟀、巴拿马箭毒蛙和海岸尖嘴鱼中雄性对繁殖的投资则远远高于雌性),在选择配偶时会表现得更加敏锐和谨慎。相反,投资较少的一方则不那么挑剔,但他们会表现出很强的同性竞争倾向,主要是为了争夺更有价值的异性。
该理论预测女性比较看重男性身上与获得资源相关的特定品质,比如社会地位,智力,较大的年龄——女性进化了对资源富足型男性的特定偏好。(从这个意义上,现代社会舆论虽然批评拜金的女性,但事实上这种心理有根深蒂固的繁殖意义——一个拥有更多资源的配偶往往意味着子女可以有更高的存活率和更大的成长空间,从而有益于该心理机制的基因的得以存续)处于支配地位的男性更受女性青睐,在一夫多妻制的社会背景中,一个女人宁可与其他女性共侍一夫(当然是地位高资源多的男性),也不愿意和一个地位低资源少的男性结婚。尽管相比前者女性只能获得配偶的部分资源,而后者可以获得全部资源,但是即便如此,女性还是更愿意选择前者。造成的结果是,拥有较少资源的男性远远无法与拥有较多资源的男性进行竞争,最终往往是少数拥有高社会地位和大量资源的人(皇帝,官员,富商等)拥有数量繁多的配偶,而大量处于社会底层的男性则无法拥有配偶。从这个意义上讲,一夫一妻制的社会为大量原来无法找到配偶的男性提供了更多择偶机会,其对女性的好处或许远没有对男性的好处大。
总而言之,进化心理学提供了这样一种观点:人类的许多共通的行为方式和心理机制都是为了解决生存和繁殖过程中遇到的适应性问题。通过这样一种观点来审视自身的行为和心理机制,其实挺有趣味的。: )

简述游戏逻辑及编辑器的抽象

最近一段时间以来,本人参与了公司下一代游戏编辑器的开发,从而有机会针对编辑器设计做一些简单的思考——如何设计更好的抽象,从而达到在客户端,服务端,以及游戏编辑器中复用尽可能多的代码?如何能够尽可能的缩短游戏设计师(策划)及美术设计师(3D/2D场景美术)的工作流程?市面上优秀的引擎往往都附带有所见即所得编辑器,这样的编辑器应当如何设计?网络游戏编辑器又有哪些可以从中借鉴和学习之处?
将尚不是很成熟的思考结果总结成本文。本人水平所限,许多错漏之处难免考虑不周全,如果有同学对本文所述的问题有任何想法,亦或是有其他文章与此相关,欢迎一起交流。

客户端逻辑的复用
把游戏客户端进行拆解,可以将其看成一个拥有输入、输出及内部逻辑循环的系统。玩家通过鼠标键盘发送消息给客户端,服务器通过网络接口发送消息给客户端,然后客户端于每一帧把当前对应的表现绘制到屏幕上。
客户端输入包括:Windows消息(键盘,鼠标,windows的其他消息,快捷键,定时器,摇杆等),网络消息(通过服务器或者其他玩家发送来的消息);输出包括:屏幕显示,网络消息(发送给服务器的响应或请求)。
事实上一个游戏编辑器也可以简单的看成类似于上述的系统,其输入包括:Windows消息(键盘,鼠标,windows的其他消息,菜单快捷键等,定时器等);输出则包括了屏幕显示,正常情况下编辑器不需要接受或发出任何网络消息(这里指与服务器进行逻辑上的通信,而非指通过版本控制系统进行数据的同步管理)。
实际上编辑器与客户端程序实际上都拥有相似的输入接口(Windows消息),只是同样的消息引发了不同的逻辑——在这里我们可以对输入接口做以抽象,并使用不同的实现来分别实现其逻辑,从而达到在编辑器中拥有热切换编辑状态和游戏状态逻辑的能力。(许多知名的游戏引擎都拥有类似上述的可在编辑器中切换编辑/游戏状态的功能,比如Crysis的SandBox编辑器,再如RunicGames的TorchLight编辑器等)。
具体来说,通过设计一个类似如下的接口(只考虑位于单个窗口中操作的情况):

class Operation
{
public:
virtual bool OnLButtonDown(POINT pt, UINT uFlags) = 0;
virtual bool OnLButtonUp(POINT pt, UINT uFlags) = 0;
virtual bool OnMouseMove(POINT pt, UINT uFlags) = 0;
virtual bool OnKeyDown(UINT nChar, UINT nRepCounts, UINT nFlags) = 0;
};

(此处仅起示意作用,就不将全部的接口都列出了)
实际程序中,我们在消息处理线程中将收到的Windows消息交给该接口,该接口背后的实现是编辑器逻辑呢?(左键按下是在选取场景中的某个物体,拖动其位置等)还是客户端逻辑呢?(左键按下是在尝试点击某个NPC或者怪物,并引发下一步动作,攻击之?对话之?)我们的前端程序并不关心接口背后的实现逻辑为何种。这样我们就可以达到动态替换逻辑的目的——在编辑器中一键切换运行模式。
这样做(所见即所玩)有什么好处?好处很多——如果不能在编辑器中立即看到游戏效果,那么游戏编辑人员(国内通常为策划人员,国外的游戏开发者中颇多为关卡设计师)必须首先在编辑器中将各类物体,对象,事件安放好,然后导出数据,再在游戏客户端中找到对应场景,并一一测试在编辑器中设置的对象。这种做法相比于在编辑器中立时可见(所编辑即所玩)的设计,其缺陷在于增加了中间环节,增加了出错可能性,当出现了与预想结果不同的现象时,定位问题所在需要花费更多时间,从而降低了游戏开发效率。此外,由于游戏开发设计的需要,往往编辑器编辑的许多数据并不是游戏最终直接使用的数据,因此存在编辑器导出数据的环节,编辑器需要将编辑好的数据导出,客户端程序需要将导出后数据进行加载,导出和加载都可能存在错误(比如编辑器中新增了功能(版本增加,文件结构改变),客户端必须增加或修改对应代码才能支持,这些环节的出错,都会降低游戏开发效率)。
良好的设计架构,通过把逻辑封装在一套预定义好的接口背后,使得同一套客户端/服务端逻辑,可以被复用于编辑器中,也可以复用于客户端/服务端程序中(逻辑代码的复用存在两个级别——代码级与二进制级,如果没有平台差异,那么在合理的设计中,我们理应做到二进制级的复用——即通过一个设计好的二进制动态运行库交给不同的进程加载;某些逻辑可能只能在源码级进行复用——比如服务器端逻辑;当然倘若采用脚本语言编写逻辑,我们可以忽略这里的差别)。当我们切换编辑器模式,程序从编辑状态转入游戏状态时,接口背后的实现被替换为客户端逻辑,最终达到在编辑器中伪造出客户端操作及逻辑的目的。
除了获得所见即所玩的好处,这种将输入接口抽象的做法,还有一个额外的好处——录像重播功能。将游戏视为输入输出系统的设计,给了我们将所有的输入事件按照时间顺序记录(通过输入设备的鼠标键盘事件,系统/定时器消息,网络消息等)。如果将所有的输入事件,按照时间记录保存下来,后再按照时间顺序作为输入重新调用对应的接口,我们就获得了整个游戏过程的重演。星际争霸,以及魔兽争霸等游戏中都有录像功能,可以将游戏过程记录下来,并重新播放,应当是基于类似的技术。
更进一步,由于网络游戏中的绝大部分逻辑是运行在服务器上的,例如与NPC交互时有什么反应,NPC会说什么话,会提供什么可选任务,会提供什么服务,NPC会如何行动,怪物会如何行动,怪物被击杀主角获得什么奖励,主角有什么技能,可以做什么事情(移动,施放技能,击杀怪物,拾取物品等)。诸如此类的逻辑实际上并非客户端逻辑,而是服务器逻辑,如何在编辑器中模拟这些东西呢?
服务端逻辑的复用
不同于单机游戏——游戏逻辑运行于本机上,网络游戏往往多数逻辑都运行在服务器上。这就造成游戏开发中的困扰——编辑好的场景,NPC,怪物无法立即看到实际状况,游戏设计师们往往并不知道该NPC是否一切正常。设计师需要将编辑器中制作的场景导出给服务器端程序,服务端可能需要重启一次服务器,加载这部分数据,然后设计者再从客户端启动游戏,连接服务器并测试新加入的内容。(这其中的流程包括,编辑器导出数据给服务端,服务端重启,服务端读取新数据,启动客户端并连接服务器:每一个步骤都可能存在其他因素引入的错误,降低开发效率;类似的道理,当出现表现出来的状况与预想不同时,游戏设计师难以定位问题所在)。
由于本人没有编写过服务端代码,对网络游戏服务端的架构并不是很熟悉,因此以下的分析仅从个人理想化的角度出发,简单谈谈服务端逻辑在编辑器中的整合。服务端运行的游戏逻辑,主要包括玩家逻辑(客户端发送的让玩家做某某事的请求,其他玩家的状态广播,玩家状态改变,玩家数据改变),怪物逻辑(怪物的行为,技能,AI,掉落以及击杀奖励等),技能逻辑(技能的时间,伤害,状态等),可达地点判定(什么地方可以到达,什么地方不能走)。服务端逻辑也可以简单的看成一个大循环,一方面通过网络收到的消息和内部触发机制创造事件,另一方面不断的循环处理事件(期间可能要读数据库,读服务器数据等),并最终做以输出——通过网络发送消息给玩家,或者写数据库。
将服务端进行拆解:我们得到一个读取(从数据文件/网络/数据库),逻辑循环(处理事件),输出(写文件/写数据库/发送网络消息)的模型。其中数据文件——往往是由游戏设计师在场景中编辑的数据,或者技能数据,或者游戏里的其他模板(物品,人物,怪,NPC等)数据,数据库往往保存了玩家的数据,人物技能/人物等级/属性/物品等。
思考一下,我们发现,服务器逻辑所需要的游戏数据文件,各类数据模板,定时器,触发器等正是编辑器所拥有的数据。服务端所需要的网络访问,数据库访问,则可以通过Proxy模式伪造一个网络层和数据库层,让服务器逻辑运行在非真实服务器的环境下(虽然服务器逻辑会以为自己运行于服务器环境中)。这就给我们在编辑器环境中运行服务器逻辑提供了可能性。
最后,要想达到服务端代码复用,那么第一就是要求逻辑代码本身不具有任何的平台相关性——调用了LinuxAPI或者WindowsAPI都是应该避免的——理想化的做法是使用java/python之类的跨平台语言。(在同做服务端程序的同事的交流中,了解到服务端程序运行的瓶颈往往并不在于CPU计算,而在于网络IO上,因此采用非平台相关语言,未必会增加多少运行上的效率损失,反而带来了代码复用的便利性,换句话说,就算我们不采用平台无关语言编写服务端逻辑,比如采用c或者c++编写服务端逻辑,只要这部分逻辑代码不沾染任何平台相关API即可。)这么做的目的是为了服务端逻辑的跨平台复用。
设计中尚存在的问题
前面谈到的目标是好的,但事实上还存在许多尚且不确定的地方。接下来简单分析一下上述做法还有哪些问题尚未考虑周全:
1. 数据格式不统一的问题。客户端逻辑需要读取数据(美术制作的模型,凸包,场景地形等等),服务器逻辑也需要读取数据(策划设置的触发器,技能,NPC,怪等等)。这些数据虽然都是由编辑器提供的,但是基于诸多理由,编辑阶段的数据与实际运行阶段的数据完全可能并不相同。如何在运行时动态的将数据转化为客户端及服务器认可的数据是一个问题。特别的,如果采用光照图之类的技术进行渲染,编辑器在导出数据的时候,还要进行费时的光照图计算;类似的还有通过图的计算,也是颇为耗费时间的工作。如何让编辑器中热启动的服务器和客户端能够在足够短的时间里获得准备好的数据,是一个需要仔细考虑的问题。
谈到游戏数据的管理(创造,编辑,保存,读取),完全可以另列一文单独讨论,此前曾经拜读了MiloYip老大的《从头开始思考游戏数据管理系统》一文,其中提及了颇多关于游戏数据管理的思考,获益良多。在此对有兴趣的同学做以推荐。
2. 客户端逻辑并不简单只有输入逻辑,事实上还有玩家逻辑,动画播放,资源动态加载与释放,界面逻辑等等,都是复杂的模块。这些逻辑如何进行复用,接口需要如何设计,尚缺乏详尽的思考。
3. 由于本人对服务端逻辑的结构尚缺乏足够的了解,服务器逻辑的复用或存在许多未考虑的问题。尽管之前提过可以通过Proxy模式为服务器逻辑提供一个虚拟的运行环境,但是相信实际的困难依旧有许多。
简要设计图
我将上述的想法做了简单的汇总,下图是一个简要的设计概览:

上图假定服务器端运行在Linux下,客户端以及编辑器运行于Windows平台。
整体架构依赖于三个抽象接口,分别为服务端编辑器所依赖的数据库接口,客户端编辑器共同依赖的网络接口和用户输入接口。围绕这三个抽象接口,我们将许多耦合进行分解,使得服务器逻辑,客户端逻辑被独立成单独的模块,不仅可以用于服务端程序或者客户端程序当中,还可以复用于编辑器中,从而使得编辑器拥有了伪造一个服务端和客户端的能力,通过这种手段达到所编辑即所玩的目的。
(更进一步,通过将编辑器中的FakeNetwork层替换为RealNetwork网络层,可以让编辑器直接连接到实际运行的服务器上,我们便得以在编辑器中直接启动一个客户端程序)
基于上述架构进行开发时,开发人员在许多地方避免了重复代码的编写(比如服务器端逻辑决定了NPC的行为,则编辑器编写者无需在编辑器中编写任何伪造NPC行为的代码,我们可以直接获得真实的服务器端NPC的表现——通过源于实际的服务端逻辑编写者编写的服务端逻辑代码;类似的,客户端逻辑决定了界面,消息响应等,既可用于实际的客户端,也可用于编辑器中模拟客户端的行为)
后记
本文实际上是针对网络游戏编辑器设计的一个尚未成熟的思路。倘若按此思路进行实做,还有不少需要考虑、充实的细节,以及需要克服的困难。但从缩短游戏开发流程,提高代码复用率的角度,本人以为文中所述的方向应当是日后网络游戏开发的趋势。

Hermite Curve 在绘制轨迹中的应用

 
在3D游戏的粒子系统中,往往可能有这样一类需求——伴随着人的动作,一条光带划过,或者伴随着武器的劈砍动作,一条刀光划过。
如果我们简单的在每次Tick的时候采样当前的位置朝向,并创建一截新的面片,往往会形成类似于如下图的粗陋效果:

之所以会产生一截一截的感觉,是由于在每次进行采样的时候,我们只能得到当前时刻的位置信息。因此尽管在从上一次到本次采样的过程中,轨迹实际经过的路程是一条平滑的曲线,可由于每一帧之间的时间不可能小到足够提供平滑曲线的采样精度,我们所能获得的总是一截截的折线。
怎样才能够通过为数不多的关键点,创造出一条平滑的曲线来呢?
Charles Hermite 提出的Hermite Curve解决了上面的问题。通过一系列的三维空间关键点,可以得到每个点处的切线,再经过Hermite Base Function的多项式计算,则可以得到插值点。
如图:

设P1,P2为平面上的两个点,T1,T2为两点处的切线,由点及点的切线,Hermite Spline就可以给出一个平滑过渡的曲线。
Hermite Base 多项式的参数如下表,其中t是从上一个点到当前点的插值系数。上一个点处为0,当前点为1。
Ogre在其Ogre::SimpleSpline类中实现了HermiteCurve。感兴趣的同学可以做以参考。
 

 
expanded
factorized

h00(t)
t3 − 3t2 + 1
1 + 2t)(1 − t)2

h10(t)
t3 − 2t2 + t
t(1 − t)2

h01(t)
− 2t3 + 3t2
t2(3 − 2t)

h11(t)
t3 − t2
t2(t − 1)

事实上,上述做法也不过就是对Hermite Curve类型的样条曲线的利用而已。样条曲线实际上并不陌生,贝塞尔曲线就是一种我们关注最多的样条曲线。尽管其不适用于我们前述的场合——原因是贝塞尔曲线通过控制点计算出的曲线并非一条依次经过控制点的曲线。如下图所示(图片来源于:http://escience.anu.edu.au/lecture/cg/Spline/printCG.en.html):
 
参考资料:
1. Hermite Curve Interpolation. Hamburg (Germany), the 30th March 1998
2. Cubic Hermite spline

windows的消息队列与消息循环

从最初开始学写Windows应用程序以来,都免不了和Windows消息打交道,但是事实上很长时间都没能把Windows的消息机制彻底弄清楚。本文记叙了我对Windows消息机制以及线程与消息关系的理解,因为水平所限,不免会有些错漏,希望对此有了解的同学指正。
1、Windows的消息队列与消息循环
所有创建了窗口的Windows程序,都需要运行一个消息循环,我们在无数的Windows编程书籍中都可以看到这样的经典代码:

1: while (GetMessage(&msg, hWnd, 0, 0) > 0)
2: {
3: TranslateMessage(&msg);
4: DispatchMessage(&msg);
5: }

这里的hWnd就是创建的窗口句柄,上述循环会不断的把该窗口(hWnd)相关的消息取出来,并分发到消息处理函数当中。
GetMessage函数是用来获取当前线程消息队列当中的消息的,其中的第二个参数如果传递一个窗口句柄,那么就会获取该窗口相关的消息,如果传NULL,那么会将线程消息队列中所有的消息都取出来。如果创建了多个窗口,而只对其中一个窗口句柄调用GetMessage形成消息循环,那么别的窗口都会毫无响应。
这里需要补充说明一个概念:消息队列是操作系统为每个需要处理消息的线程创建的,任何线程只要调用过与消息有关的函数(如,GetMessage,PeekMessage),操作系统就会为该线程创建消息队列。可能有人会问,那没有窗口的线程,操作系统也有必要为其创建消息队列么?这是有可能的,因为我们可以通过诸如PostThreadMessage的Api向别的线程或者本线程发送消息,如果目标线程没有消息队列,会导致这个函数返回失败。
做一个验证上述想法实验:
我们撰写类似于如下代码(所有的示例代码我都已经在Windows7系统上编写程序做过实验,这里忽略与要说明内容无关的细节并不影响对原理的理解,就不再将完整代码附上,读者感兴趣的话可以自行实验)

1: HWND hWnd1 = CreateWindow(…);
2: HWND hWnd2 = CreateWindow(…);
3: while (GetMessage(&msg, hWnd1, 0, 0) > 0)
4: {
5: TranslateMessage(&msg);
6: [...]

认识预设信念与自我纠错

预设信念与自我纠错
-读《社会心理学》
我们如何认知客观事物?我们的感觉忠实的反应出世界的原貌吗?并非如此。我们总是带着偏见去看这个世界的。
每个人都有自己预设的观念,所有的外界信息,在进入大脑之后,并非呈现为对外界客观事实的真实表现,而是与我们预设的观念相结合,亦或是被我们预设的判断所过滤,成为我们对外界事实的解释而被记忆。有箴言曰:“客观事实确实存在,但是我们都是通过信念和价值观的眼镜去观察他们”。因此当外界事件发生时,我们所作出的反应并非针对“事件本身”的反应,而是针对我们“对事件的解释”而做出的反应。
价值观是什么:“这是什么”与“这应该是什么”之间的差别就是我们的价值观。换句话说,我们从对事实的客观描述偏转到对“应该如是”的说明陈述时,我们就纳入了自己的价值观。
在《学会提问——批判性思维指南》一书中提到一个例子:针对政府是否应该对儿童电视节目进行管制的问题,有两种看法:反对者认为政府不应该对儿童电视节目进行管制,其理由是监控孩子观看的电视节目只是父母的责任。而支持政府应对儿童电视节目进行管制的人认为,那些通过儿童电视节目牟利的人在节目中加了大量商业广告——会对儿童产生不良影响。
在这个例子中,反对者认为个人有能力决定自己需要什么或不需要什么——价值观假设:当前例子中个人权利比公共责任更重要;而支持者认为政府的力量对保护孩子健康成长更加重要——价值观假设:该例中公共责任比个人权利更重要。
上述例子说明了不同的价值观是如何影响我们对同一事物的判断和认知的。
下面的例子展现了预设信念对认知的影响:
A
BIRD
IN THE
THE HAND
上面的短语有错误吗?粗略的扫过一眼的话,很多时候难以发现其中的错误,这不仅需要眼睛观察,更需要知觉参与。而一旦发现了其中的错误(预设信念),再想无视这个错误就很困难了。这个例子说明了预先判断和期待对我们认知的影响。正因为我们期待着一个正常的语句,才对错误视而不见;而发现错误之后正相反。(相信很多人在为自己写的文章查找错别字时也有类似的感受,难以找到错漏之处)
这就是我们大脑的工作方式。如果大脑没有预先设定你将知觉到某个物体,它便将这个物体阻隔在你的意识之外。我们对现实的知觉会为我们的预期所左右。
更现实一些的例子比如关于中国的房价:对于尚未买房而想买房的人,对房价下跌有强烈的企盼,因此如果有人说房价会涨,那么不管他举了什么事实数据、提供了什么证据,我们都很容易对其观点产生抵触情绪,而不去核实他的观点是否有足够的理由支撑,这种情绪化的反应,让我们错过了从相反观点中获取知识和信息的机会。事实上,当持有一个预设信念时,人都较容易接受与其观点相同的证据,而极力批评和反对与其观点相悖的信息。
由于预设信念的差别,对于同一个一件客观事实,我们每个人都是透过我们自己的信息、态度和价值观去看待它。我们知道一件事情先入为主的印象往往是很重要的,一旦形成了先入为主的信念,它将影响对后续所有相关信息的知觉。
《社会心理学》中提到的一个事例正说明了观点是如何影响我们解释事物的:1951年,普林斯顿与达特茅斯大学之间的一场橄榄球赛中,双方球员起了激烈冲突。比赛结束后不久,作为社会心理学实验的一部分,分别来自两个学校的心理学家在各自的校园里为学生重放了比赛录像,要求学生以科学观察者的身份,观察注意每一次摩擦并确定哪一方对此负有责任。但是学生们却无法将对各自学校的忠诚弃之不顾。普林斯顿的学生相比达特茅斯的学生更容易认定普林斯顿的学生为受害者,而另一方也是同样,认为己方更多的是受害者。
对我们自身来说,如果不想掉入认知偏见的陷阱而不自知,就应当尽量避免被大脑中先入为主的信念所左右,而有意识的锻炼大脑对新信息、尤其是那些与我们已形成的信念相悖并为大脑自身所阻隔的信息的接收能力。
接收与自我信念相背的信息需要我们学会倾听:倾听并不像人们正常想象的那样,是一件容易的事情——只要你说我听就行了,相反,如果不通过有意识的努力,人可能永远无法学会倾听,其原因正在于大脑的阻隔效应——它会将我们不愿知觉的、与已有观点相悖的、引发不快的信息隔绝在意识之外。
倾听并不是说要对他人所说的东西全盘接受,而是尽可能的从他人所述的话语中,抓取对方的观点,理清对方用于支撑观点的论据,对方的推理所隐含的价值观假设。从而我们可以尽量全面的收集信息,以作出更加客观的判断。
纠正错误的预设信念的唯一方法则是解释相反观点。通过问自己:“假设我是一个持相反观点的人,我是否会在这个问题上同那些与我观点不同的人得出同样的结论呢?”——通过解释与自己相左的观点也可能是正确的,(如果我认为房价会下跌,那么通过思考为什么房价可能上涨?反之,思考为什么房价可能下跌。)有助于降低甚至消除预先存在的信念固着(belief perseverance)。
事实上,对一件事物有各种可能的解释,不一定其他解释就是相反的观点,通过站在更多的立场上思考,会促使我们尽量仔细的考虑不同的可能。
消除错误观念的时候,还往往需要弄正确区分“我认为事实应当如何”与“事实实际如何”的差别,有的时候我们不得不承认事实与信念相左:尽管我认为事实应当如何,但是事实实际上并非如此。更进一步,我们在作出判断时,所需要纳入考虑的是事实实际如何——我们认为事实应当如何与我们当前所做的判断没有也不应有任何关系。

大学期间应当做的三件事

大学期间应当做的三件事
——读《我是一只IT小小鸟》
最初得知这本书,首先还是看到pongba和徐宥在TopLanguage讨论组上分别贴出的两篇文章《我在南大的七年》,以及《我的大学》。读完之后,收获良多,并且颇为遗憾自己大学期间浪费了太多时间。
书中挑选的大都是在大家身边就可见到的一些牛人,读了他们的故事,你会发现牛人也并非遥不可及。人家牛有牛的道理,知识也好,能力也好,一切都可以提前准备,都是可以积累的并且也只能是通过积累得来的。(参考:李笑来1,李笑来2)
如何积累?结合最近关注的一些知识,我总结了几条积累的方法。其实无论是对于进入大学读书的学生来说也好,还是已经毕业参加了工作的人来说也好,以下几个积累的办法都是值得去实践的(而积累这种事情,当然是越早越好,大学期间有得天独厚的条件——充裕的时间):
1、  读书、读好书并且大量阅读。这里的好书的意思是指:a) 自身感兴趣 b) 作者是大牛(譬如诺贝尔奖获得者的经典代表作)c) 豆瓣、亚马逊(amazon.com而非amazon.cn)上的星评很高,读者评价很好的书。上述三个判断标准,也是找好书的方法,关于怎么找好书,参考pongba的《一直伴随我的一些学习习惯》
比如,徐宥之所以能够在本科毕业时获得远比同届人多得多的机会(拿到微软、Google的Offer,考上北大的研,拿到华盛顿大学的Offer),和他在大学期间的海量阅读是分不开的(他在自述中提到他读完了南大图书馆的TP312书架)。
2、  学会正确的思考。这里特意强调正确的意义在于,我们绝大多数人都不会正确思考,想学会正确的思考,需要自己努力挣扎。
正确的思考的第一步需要做到独立思考,对立于独立思考的是不做思考或直接对外界的信息(或者支撑自己观点的信息)不加批判的接受。(参考笑来老师)套用笑来老师的原话“人家说什么你就信什么,挺傻的。”
记得李开复在《做最好的自己》中曾经举过一个例子来说明中国的大学生缺乏自己思考并为自己做决定的能力:“读高中的时候,父母给我的目标就是考大学,考上大学做什么,我没有想过;……有些学生在来信中直截了当的说:‘只有你能告诉我,我该怎么做。’——他们宁肯被动的接受建议,也不愿意花点功夫设计属于自己的成功之路。”
为什么独立思考的能力这么重要?如今的社会上的信息泛滥,各种观点都存在,如果没有独立思考的能力,就无法正确的获取自己所需的信息,以及对信息做必要的加工从而得出自己的结论。
当拥有了独立思考的能力之后,你会发现世界在你面前有了巨大的变化,之前和之后,如果说的通俗点,就是“没脑子”和“有脑子”的差别——这里意指有脑子但是不去使用(独立思考),那么跟没脑子也就没什么差别。
从此以后,外界的纷繁复杂的信息不再能直接左右你了,然而你依旧会发现自己还是不能完全鉴别这些信息,比如一个观点感觉上是错的,那它为什么错?错在哪里?是前提不正确还是推理的过程有缺陷?还是推理的过程中引用的论据本身不成立?当我们第一次尝试去独立思考的时候,都会遇到这么一系列问题,因为我们长期疏于使用自己的大脑去思考,我们发现自己难以从别人的错误观点中找出不合逻辑的地方来。
因此,正确思考的第二步是掌握鉴别信息的方法:批判性思维(Critical Thinking),实际这个翻译并不那么贴切——按照笑来老师的说法,Critical Thinking最贴切的含义是“想明白、想清楚、清楚思考”(参考笑来老师的《想明白》系列文章)。有了方法之后,我们只有通过使用,不断的观察、思考去提升自己的思考能力,反之,有了方法不用与有脑子不用本质上也是一样的。
3、  开阔视野
只有视野足够开阔,才能知道自己真正想要的是什么,在做决策的时候才能站在更高的层面考虑问题。从某种意义上来说,也可以避免多走很多弯路。
尤其对于初入大学的学生来说,未来四年的时间如何规划安排?
是安安静静的按照学校的课程计划上课、写作业、考试?
还是把大块的时间用来做自己感兴趣的事,学自己感兴趣的知识?
有人说课程很重要,计算机的体系结构的知识都在学校安排的课程当中;也有人说上课太浪费时间,完全可以通过自学(读大量的经典书籍)掌握所有应该掌握的知识,并且自学的自主性会让你更加有兴趣;究竟应该如何做?——那是应该独立思考的,不是么?: )
在大一的时候曾经有一位能力很强的学长对我说过这样一段话:“对于所有人来说,一个学期都是17周、或者18周,但是有的人就非常牛,比如参加软件项目,ACM竞赛拿奖,而有的人似乎什么都没做,最后大家的期末成绩也差不开太多,原因在哪?——原因在于,决定期末考试成绩的只是考试前的2周时间而已,你和人家的差别在那剩下的15周里。”
我一直非常感激这位学长,这是点醒我的一番话,因为大一时接触的信息很局限,视野也很狭隘,我的大部分精力都用在对课程本身的学习上(除此之外大都荒废在玩上了),甚至没有发现我还有别的选择(比如把大量的时间用在读经典书籍、学习自己感兴趣的知识、参与实际项目上)。
视野不够开阔的问题在于:如果你看不到你还有别的选择,那么如何去做独立的判断和思考呢?

最后具体说说,如何开阔视野?
简单来说,网络上什么都找得到。
比如你钟爱某些技术类书籍,那么就去看作者的博客(比如写《Modern C++ Design》的作者Andrei Alexandrescu;比如著名的著译者侯捷;比如著名的Bjarne Stroustrup),看看他们在关注些什么,最近在做些什么,这些牛人可以极大的拓展你的视野;
你喜欢C++,那么就去加入到C++的讨论组中学习,类似这样的讨论组,气氛通常都比较活跃,从中你可以了解到很多知识,以及语言最新的动态;
喜欢Linux喜欢开源?你可以找一个你感兴趣的开源项目,把代码checkout到本地,自己编译一个试一试,再看看源码结构,从中了解一下大型的软件项目是什么样子,更进一步,或许你在使用软件的过程中发现什么缺陷,你也可以尝试向开源项目贡献代码,从开源社区中学习,也可以极大的拓展你的视野;
甚至你说对上述的几个都不感兴趣,只喜欢看动漫,那也可以参与到你喜欢看的动漫的字幕组当中去,尝试一下做日语翻译或者校对、时间轴的工作,即便是这些也会给你带来很多意想不到的好处……
如果你想过一个充实而有意义的大学生活,不妨也去读读《我是一只IT小小鸟》。

Win32平台下编译SVN源码全过程

前段时间曾经总结过一些在win32平台下基于SVN开发的一些注意事项,主要是在利用svn官方发布的二进制库进行开发过程中使用的方法和一些值得注意的问题。
 
由于svn官方发布的win32平台下的二进制文件是基于vc6编译的,在使用vc2005进行开发时,会遇到因CRT冲突而引起的link错误。因此,如果是使用vc2005(我推测使用VC2003也会遇到同样的问题,尚未验证)附带的CRT库与svn官方发布的binary进行link,那么无论如何都会出现crash的问题。最为彻底的解决方案,还是自行编译svn源码。
 
在win32下编译svn源码说明:
 首先需要从官方下载一份SVN源码,版本可以根据需要选取,比如最新的Release 1.6.3(目前已经更新到1.6.6)可以在这个地址下载到:
http://subversion.tigris.org/downloads/subversion-1.6.3.zip
以及svn所需的依赖包:
http://subversion.tigris.org/downloads/subversion-deps-1.6.3.zip
如果需要支持ssl的话,还需要下载openssl(根据实际需要选择相应版本):
http://www.openssl.org/source/
如果需要BerkeleyDB的话,需要下载WindowsBDB(BDB是可选的,如果不使用BDB,则默认使用FSFS):
http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=688&expandFolder=688&folderID=2627
此外还有Windows libintl:
http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=2627&expandFolder=2627&folderID=8100
 
 
解压svn源码包,可以在subversion-1.6.3目录中找到这么一个官方发布的说明文件INSTALL。该文件详述了安装通过源码编译SVN所需依赖的工具及第三方库,并且给出了详细的步骤。
网上同样有一个INSTALL的说明,可以在这里访问到:
http://svn.collab.net/repos/svn/trunk/INSTALL
 
编译开源项目的话,其附带的INSTALL说明都是最重要也是最全面的参考。网上搜索的其他资料,也会有相应的参考价值,但无论如何,其信息的来源也是INSTALL。因此编译开源项目时,认真阅读INSTALL是最重要且效率最高的。
 
从Google上进行一下简单的搜索的话,可以找到一篇介绍svn源码编译的文章,在这里:http://rocksun.cn/?p=103
 
为了后续的步骤方便,我们需要先准备编译所必须的一些东西:
包括Perl:http://www.activestate.com/activeperl/
Python:http://www.python.org/download/
 
 
安装Python以及Perl,比如分别安装到D:\Python26以及D:\Perl。安装好之后,将python以及perl的bin目录设为系统目录并重新启动使之生效。
 
编译svn源码第一步,将下载的svn源码包解压到X:\SVN\svn—trunk下。X可以是任意一个盘符。在我的机器上,我使用了F盘,下文中皆以F盘举例。
 
在F:\SVN\svn-trunk中解压了刚才下载的svn源码包以及依赖之后,可以在目录
F:\SVN\src-trunk\subversion-1.6.3中看到以下文件及目录:

以及文件:

Svn的编译需要依赖libapr以及libapr-util,SQLite,zlib,libintl可选,libneon/libserf二则择一,openssl可选,BerkeleyDB可选,libsasl可选,对Python,Perl,Java,Ruby支持的模块可选,以及KDELibs,GNOME Keyring可选。
 
我们依次先编译依赖项:首先进入到subversion-1.6.3\apr目录中。可以看到存在apr.dsw以及apr.dsp文件,这是VC6的工程文件。我们如果想在2005下编译的话,需要将其转换成sln及vcproj文件,简单的用vc2005打开该文件并保存即可。该目录下还有Makefile.win文件,是win下的makefile,我们打开makefile.win文件查看一下说明:可以得知如果需要编译.sln文件的话,需要置USESLN=1。
 
在VC2005的命令行中输入nmake -f makefile.win buildall checkall USESLN=1便可以开始编译apr了。Checkall表示编译完成后会去运行所有的测试用例。
 
编译完成后,当前目录下会多出2个文件夹,分别是LibR – StaticRelease,Release – DllRelease。如果选择Debug编译,则会生成LibD – StaticDebug, Debug – DllDebug。
 
类似的,我们将apr-util以及apr-iconv也编译好。
 
编译zlib:
进入zlib目录后,使用以下命令编译zlib库
nmake -f win32/Makefile.msc
 
编译openssl:
将此前下载的openssl解压到F:\SVN\openssl
阅读其INSTALL文档(INSTALL.W32)
使用VC编译openssl首先需要运行configure:
perl Configure VC-WIN32
接着运行
ms\do_masm
这里的do_masm是一个bat脚本,该脚本会生成nt.mak以及ntdll.mak分别是Release版本的静态和动态的库的make文件。如果想生成debug版本的make文件,可以通过修改do_masm.bat中的调用mk1mf.pl脚本处的参数实现,具体参数可以参考mk1mf.pl文件自身的说明。
接下来,创建动态链接库版本的ssl库用nmake -f ms\ntdll.mak,以及静态版本使用:nmake -f ms\nt.mak
生成的结果文件位于out32dll文件夹,以及out32文件夹中。
 
编译neon
进入F:\SVN\src-trunk\subversion-1.6.3\neon目录
nmake –f neon.mak
默认生成的是release版的libneon.lib (debug版为libneonD.lib)
可以用nmake –f neon.mak DEBUG_BUILD=1生成debug版的lib。
 
回到F:\SVN\src-trunk\subversion-1.6.3
运行python gen-make.py –help可以了解如何使用gen-make.py生成我们所需的svn编译文件。
由于在此,我打算选用neon, libintl, openssl(本例中并不打算使用BDB,如果需要BDB则需要增加—with-berkeley-db=DIR参数)进行编译,目前需要关注的几个重要参数如下:
–with-apr=DIR
–with-apr-util=DIR
–with-apr-iconv=DIR
–with-neon=DIR
–with-libintl=DIR
–with-openssl=DIR
–with-zlib=DIR
–vsnet-version=VER
 
运行
F:\SVN\src-trunk\subversion-1.6.3>python gen-make.py -t vcproj
–with-apr=apr –with-apr-iconv=apr-iconv –with-apr-util=apr-util –with-libintl=svn-win32-libintl –with-openssl=..\..\openssl –with-zlib=zlib –vsnet-version=2005
 
(其中的libintl需要解压到当前文件夹中)
即可生成vc2005的sln(subversion_vcnet.sln)文件了。
 
打开vc2005,选择Debug编译选项,对项目ALL进行编译。如果一切顺利,则会生成一个F:\SVN\src-trunk\subversion-1.6.3\Debug目录。内容包括svn的所有的lib及可执行文件。

将svn\svn.exe以及所有目录下的.dll文件拷贝到一个新建的bin目录下。将openssl的dll,apr, apr-util, apr-iconv的dll拷贝到同样的bin目录下,如下图:

运行svn –version看看结果吧~
 
最后,当我们有了自行编译的svn可以做什么?你可以做任何你想做的事——比如自己基于svn的接口进行开发(可以参考开源项目rapidsvn以及TortoiseSVN的源码)

读书笔记——《思维改变生活》

我们对自身生活状况的满意程度乃至于生活本身,都取决于我们对事物的认知。
 
认知由人头脑中的思维,“想法”(thoughts)和“观念”(belief)组成。
我们的全部认知来源于我们的感知(童年经历,打过交道的人,人生经历,书本,媒体以及生物学倾向性biological predisposition)
 
认定生活(事情)必须(应该)是某个样子的,就必然给你带来烦恼。 因为,根据认知科学的理论:并不是人和事让我们喜悦或悲伤——它们只不过是提供了一种刺激。其实是我们的认知决定了我们在特定情况下的感受。(ABC模型:A(前因:antecedent),B(观念:belief),C(结果consequences))这要求我们学会辨认思维中消极荒谬的观念,并且学会质疑这些荒谬、消极的观点。
 
人们天生就倾向于用不合理的、自我挫败的方式来思考。(什么样的思维是不合理的?如果我们的思维违背了我们追求生存与幸福的内在欲求,那么它就是不合理的)书中罗列了几种常见的错误思维方式:
非黑即白的思维;
    以偏概全(overgeneralisation):(有时候仅凭一次经历,我们就用“总是”、“从来不”、“每个人”);
    自找罪受(我们觉得自己应该为某事负责,然而该事情并不是我们造成的;或者我们呢错误的以为别人的言行是针对我们的——我们容易感到愤愤不平,容易把别人的言行视为粗暴无礼,并且以牙还牙);
    心理滤除(mental filter)关于自身、他人和世界的信条可以使我们对自身经历的感知出现偏差。如果信息与我们已有的信条不一致,我们就将其过滤掉。 (证实偏差,人们倾向于寻找能够确认或者支持自己已有观念的证据)
    草率得出负面结论(jumping to negative conclusions):很多人倾向于对各种情况得出负面结论,而不管支持这种结论的依据是多么有限。
    贴标签(labeling):仅凭一两个行为或者一两个特点就概括了整个人(ref to 以偏概全),重要的是区分一个人的某些行为并不是这个人的全部。
 
很多时候,我们从逻辑上明白自己的思维是消极的或者不合逻辑的,但是在内心里却很难加以改变。这是由于逻辑等高级思维体验源于大脑额页的皮层,而情感体验源于杏仁核,外部刺激首先传导到负责情感的区域,激起兴奋或者恐惧,然后才为负责逻辑的额页所处理,因此我们在遇到意料之外的问题时,往往会首先感到兴奋、愤怒、惊恐,并在这些情感的刺激下做出行动,从而把理性以及逻辑抛之一旁。这种应激情绪反应在进化当中是有其积极意义的,比如当遭遇猛兽时感到巨大的恐惧而立即逃跑,可能会有利于生存。
归根结底,是进化赋予我们的神经系统,决定了我们总是会遇到逻辑与情感的冲突。(ref. 《找寻逝去的自我》,《欲望之源》)但这并不是说我们对此无能为力,认识到情感与逻辑冲突的内在原因,就是改变生活的第一步。
 
当我们得不到自己想要的东西时,会体验到挫折。有一种频繁造成挫折感和自我打击的思维模式,就是认为本已发生的事不该发生。如果从符合逻辑的角度思考,我们从已经发生的事情中吸取教训,以避免再次犯同样的错误,这是有用的;但是,很多时候,我们只是不断告诉自己不该做我们已经做过的事情,这既无意义,也不合逻辑。
 
当我们遭到不公平对待时,会体验到愤怒。愤怒可以引发搏斗或逃跑反应(fight-or-flight response),数百万年的进化历程中,这一反应为我们的祖先提供了所需的额外能量储备,让他们要么与野兽搏斗,要么逃跑。搏斗或逃跑反应在体内引发一些生理变化:为了增加我们对氧气的摄取和消耗,并提供可以迅速获取的能量。
然而在当今社会中,这种愤怒引发的附加反应越来越不适用了。
愤怒导致我们无法清醒而理性的思考,使得注意力从问题本身移开,而关注自己受到的侵犯、不公以及别人的邪恶。同时,愤怒是最能激发生理反应的情绪之一,长期激动导致的生理变化会损害健康。再次,愤怒常常表现为敌对或攻击行为,破坏我们的人际关系。
事实上,不公正可能只是我们基于自身考虑的想法:尽管我们执着于自己的观点,但是我们的观点却不一定正确或全面,而只是看待当前状况的众多方式的一种。通常是我们觉得重要的规则遭到了破坏,才觉得愤怒,因此,是我们自己使自己愤怒,外部发生的事情是刺激,是我们的认知决定了我们在特定情况下的感受,倘若我们不认为事情非如何不可,那么愤怒便不会产生。
 
 
当我们预期坏事情将要发生时,感到的担忧与畏惧,是焦虑。进化促进了焦虑:对于动物和人来说,在进化的历史上,焦虑层有利于生存——焦虑提高了我们探测环境中威胁的能力,并让我们迅速增加能量,逃离那些威胁。然而事实上,在现代社会中,焦虑增加了我们的痛苦,因为,我们大部分痛苦实际来自于对事情的负面预期而非事件本身。通过面对自己的恐惧,可以降低焦虑,反之,逃避问题会强化我们觉得这类情景很危险,从而降低我们在未来遭遇这类情境时的应对能力。
 
 
很多时候,遭遇冲突或者遇到尴尬情况时,我们需要沟通以解决问题。然而,很多时候,我们不愿意传递全面的信息——我们可能会指出问题的存在,却不说出自己的感受或想法;或者我们本应做出严正的声明,却只是提出了一个问题。我们提供片面的信息是为了避免发生冲突或者受到非议。虽然我们也想解决问题,但是我们没有信心进行坦率的沟通,所以我们希望别人能够领会我们的暗示和模棱两可的话。
 
有效沟通的习惯:
1,果敢坦率:沟通的要点在于“我们的权益都很重要——让我们相互理解吧”。
2,愿意和解:我们并不一定要满足自己的要求,我们只是想交流自己对事情的看法——愿意和解=我想让事情对大家都公平。
3,说出来:当我们发现别人说话做事让我们感到不愉快时,最好指出这一点。把我们的想法感受要求告诉别人,这就为别人提供了反馈,让别人知道他们的言行对我们意味着什么。4,“我”字开头陈述:典型的“我”字句会描述别人对我们造成问题的行为,描述我们的感受,还描述我们希望看到的另一种行为——我字句的一大好处就是他们对事不对人:这种区分是重要的,因为行为容易改变。当我们关注别人的行为,我们并不是在攻击他们本人。
5,提供完整信息(包括观察到的客观事实,我们的想法,我们的感受,我们希望看到的情况):完整的信息可以提供一个沟通的框架,让我们清楚、理性、平和的沟通。
 
通过思维改变生活的关键在于——
通过逻辑,理性的判断,对生活中发生的事情(刺激)做出适当的反应,学会控制自己的大脑,而不是任由大脑中自远古进化中所得来的情绪反应支配。
反思我们支配时间的方式:在大部分人的生活中,知道应该怎么做与实际上怎么做并没有太大联系。我们应当扪心自问,我们的生活方式是否与我们的想法一致?

编写一个DLL时应当注意什么

库与代码重用
1、静态库vs动态库
静态库的优势和劣势
动态库的优势与问题
2、静态C/C++运行库 vs 动态C/C++运行库 & manifest
3、关于动态库接口设计
 
 
库与代码重用
 
对于像C,C++这样的语言来说,库是扩展语言功能的重要手段,对于项目来说,代码库是节约开发时间,缩减成本,划分项目模块以利于多人合作,并通过重用已有代码而避免重复劳动的必要工具。开发一个稍具规模的项目,总免不了设计和划分模块,如何设计和划分,采取什么方案解决问题,总要面临诸多取舍。
最近跟动态库/静态库/动态静态CRT/MFC打了诸多交道。本文是对这段时间遇到的问题与思考的总结。
 
1.       静态库 vs. 动态库
 
静态库本质上就是编译好的目标代码(使用vc的话,编译.cpp文件或者.c文件之后会得到目标文件.obj,编译结束后,link程序会按照指定的要求将目标文件链接成最终的输出文件——常见的有.lib/.dll/.exe)的一份合集。
相比于动态库,静态库没有入口函数,不能独立执行,不能动态链接,一旦被链接到目标文件中,静态库的代码段/数据段都会被合并到目标文件当中去。这是静态库与动态库的本质区别。
 
由于上述差别,那么我们就很容易理解下面的情况:如果你在静态库中定义一个静态或全局对象,最终该对象会被链接到目标文件的数据段中(目标文件可能是某个dll也可能是exe)。
 
静态库的优势在于,简单易用,你无需考虑内存的申请与释放问题,因为静态库的代码段作为目标模块的一部分(EXE或DLL),在其中所调用的new/delete在链接阶段会被重定位到该目标模块的CRT函数地址。对应的,如果你使用DLL,由于DLL中会自行初始化一份CRT堆,因此它的内存申请与释放是在有别于在EXE的CRT堆中进行的(两个不同的CRT堆),如果你从DLL中申请一块内存出来使用,最后也必须交还给DLL模块内部去释放。否则会产生Heap Collision,在Debug模式下,你会收到一个系统位于malloc中的断言,并了解到发生了什么。
再有,如果你的静态库A依赖于静态库B的头文件,而目标应用程序EXE依赖了静态库A,那么你在链接静态库A的时候,也必须链接静态库B。否则会有静态库A中的某些符号无法被找到,从而导致链接失败。而动态库A如果依赖于一个静态库B,那么你无需在依赖该动态库的EXE中再次包含该静态库,因为该动态库A已经将该静态库B链接到A模块当中了,在DLL A中已经有了一份,如果EXE模块没有直接依赖于静态库B的话,那么就无需在EXE中再次link该静态库了。
 
上述问题也通常是混用静态库和动态库时可能遇到的一个问题:如果有一个静态库A,被动态库B和EXE C都用到了,而EXE C又需要使用动态库B,那么这份A的代码和全局数据就在动态库B和EXE C中存在了两份。第一,这是一种对空间的浪费;其次,这可能会引发问题,比如,当我在EXE C中new一份A中的某对象X的指针,并将该指针传入到动态库B中使用,表面上看似乎没有问题,但是如果这个对象X引用了A库中的全局/静态数据,就可能会引发问题,因为此时动态库B中存在有一份A库中的全局/静态数据,而EXE C中有另一份,如果这个全局/静态数据被X使用的话,那么就会得到前后不一致的上下文环境。具体可能产生什么问题要看具体的逻辑是如何组织的。在这种情况下,最好能将静态库A重新组织成一个动态库,这样就不存在多份代码以及多套全局数据的问题了;如果不能这么做,你可以根据具体的应用程序组织的形式,采取DLL导出A库接口,EXE使用DLL导出接口去访问A库而不直接链接A库本身的方法来变通,但通常这不是根本的解决方案。
 
另一方面,在DLL中可以包含自己的资源文件,而LIB中不能。因为DLL是一份完整的PE文件,而LIB只是若干个OBJ打包。这并不是说,使用LIB的时候不能使用资源(RC),你可以将LIB中使用的RC文件,resource.h都include到目标应用程序当中,只要没有资源ID冲突就可以正常使用,就像是你在目标应用程序的资源当中定义了这些资源一样。而DLL中的资源则隶属于DLL自身,不会与EXE中的资源在ID号上产生冲突,只是在使用的时候还需要针对Resource做一些Handle上的设置(参考AfxSetResourceHandle),才能使得API访问DLL中的资源而非EXE中的。如果要编写一个带有资源的库模块,DLL确实是不二选择。
 
        
2.       静态&动态C/C++ runtime 以及 manifest
 
静态的C-runtime库叫做LIBC.lib,我们通常使用多线程的版本叫LIBCMT.lib,debug下的版本叫LIBCMTD.lib。而动态的C-runtime,在VC的环境下,我们使用的是MSVCRT.lib,以及debug下的MSVCRTD.lib(这两个lib实际是两个导出符号文件,实际的运行库位于msvcrt.dll/msvcrtd.dll中(VC6),如果是VC2005的版本则是msvcr80.dll/msvcr80d.dll)
C++的runtime库,静态版的是LIBCPMT.lib/LIBCPMTD.lib。动态版的是MSVCPRT.lib以及MSVCPRTD.lib。(类似的,实际的运行库位于<VC2005中>msvcp80.dll/msvcp80d.dll)
 
在第一点的对比中已经提到过,如果同时又DLL模块依赖一个静态库,而又有一个依赖该DLL模块的EXE还依赖这个静态库,那么最终的内存中就会存在两份该静态库的代码以及全局数据。这一点对于C/C++运行库也是一样的。所需要注意的是,只要是基于C语言以及C++语言的程序,几乎所有的都会用到C/C++运行库,因此如果在规划一个项目的时候,发现有多个dll模块,那么最好是使用动态版的运行库,以避免最终的内存中出现多份不必要的运行库代码。
 
再有一点是关于XP之后的系统中,以及在VC2005之后的编译环境中,微软添加的manifest机制。该机制是为了解决同名但版本错误的DLL加载问题而提出的。比如,我们手上有两份VC编译器,分别是不同的版本,其中携带的MSVCR80.dll也是不同的版本,但是都叫MSVCR80.dll,如果我们在应用程序安装时,简单粗暴的将系统目录下替换成我们应用程序所依赖的MSVCR80.dll,那么可能会造成以往的应用程序无法正确运行的问题,因为他们可能依赖的是其他版本的MSVCR80.dll。
解决这个问题的途径是将一个dll的运行环境,版本,以及校验值等信息编码到一个目录名当中,而在安装应用程序时,将该版本的dll放置到对应名字的目录下,这样尽管dll名称一致,我们也可以找到所需要正确版本的dll。但是应用程序怎么知道需要依赖哪个版本的dll呢?如果应用程序不知道,即使系统中有正确版本的dll,操作系统也不知道应该去加载哪一个dll。
因此manifest文件就把该模块所依赖的模块都标明了,通过名称,校验码,版本等等信息以确保所指明的模块不会有歧义(用记事本打开一个manifest文件,你就可以看到上述信息),当该应用程序EXE或者DLL被操作系统加载时,系统就会根据该信息去查找对应模块,首先会在C:\Windows\WinSxS目录下找(打开这个目录,你就能看到大量不同版本的CRT/CPRT/MFC/ATL等等运行库),如果没有找到那么也会应用程序当前目录下找。
 
这就是为什么使用VC2005编译一个dll也好,一个exe也好,总会多生成一个manifest的文件的原因。所以如果使用了动态版本的CRT运行库,或者其他任何DLL,那么最好就不要关闭manifest生成的编译选项。
 
3.       关于动态库接口设计
 
如果动态库可能经常会更新版本,提供新的功能接口,那么如果使得动态库的升级无需改动依赖于他的可执行模块就是一个值得思考的问题。
具体要达到上述目标,需要保证的有以下几点:
a.       动态库中的类的内存布局不暴露给外界,也就是说动态库提供给用户的.h中,不应当有类的成员信息。这是因为,如果类的内部数据修改了,增加了一个变量或者减少了,那么这个类所占用的内存大小也就修改了,倘若有exe使用上一版本的类定义,new了一个类对象,那么一旦发生类定义修改的情况,这部分代码就必须要重新编译链接了。比较标准的做法是dll只向外界提供接口定义,而不提供实现定义,即提供的是一个类,但是类中只包含纯虚函数,而没有数据<这种做法有点类似于COM的思路>;或者是只提供一系列普通函数以及一个实现类的指针,这种做法有一个好处,如果添加新的函数接口,也无需重新更新外部程序,但是如果使用虚函数接口则不然。
b.       动态库中的类应当不给外界提供构造析构的能力,同样是基于上述原因,如果外界可以new/delete该类,那么当类的定义修改时就会出现不得不重新编译使用该库的应用程序的情况。倘若只通过dll中定义好的借口进行交互,则不存在这个问题。比如外部不能直接new一个对象,而是必须从一个dll导出函数中获取一个对象的指针,外部同样不能delete这个对象,而是必须将之送入另一个导出函数交给dll去删除。这样只要dll保持着两个接口不变,更新dll时是无需更新依赖该dll的应用程序的。(一个比较好的做法是将dll提供外界的接口类的构造析构函数设置为private)
c.       Dll应当尽可能使用动态crt,如果用到了mfc应当尽可能使用动态mfc。这是为了避免dll与外部模块中存在多份重复代码的情况。同时如果有dll需要依赖的其他静态库,也应当尽可能使用该库的动态版本。
 
p.s. 如果想了解更多的关于静态库,动态库,C运行库方面的知识,可以参考《程序员的自我修养——链接装载与库》一书。
p.s.2 果然如pongba老大所言,将自己认为没有问题的知识写下来是一次很好的学习过程,写下本篇文章的过程中,我将原先认为明白的,但是实际并不见得就真的明白的问题,梳理清楚了很多。写的过程有助于把原先大脑中下意识存在的假设显现出来,在思维中再次加工,将原先不明确的概念搞清楚。

3D地形学习笔记1–地形生成

地形生成是一个很有意思的话题,通过简单的函数以及一些参数配置,就可以轻松的生成接近于自然地貌的地形。
 
两种众所周知的地形生成算法分别是
1、  Fault Formation
2、  Midponit displacement (又名diamond square)
 
Fault Formation 算法
这个算法的本质其实很简单。想象一个方块,在这个方块中随机取一个点为起始点,再随机选择一个方向,形成一条射线,然后将高度图中这个射线左侧(或者右侧)的部分全部添加一个Delta。重复上述步骤若干次,每次将添加的Delta减小一些,一个简单的高度图就可以生成出来了。
Delta应当与当前迭代次数与总迭代次数的比值成线性关系。
Delta = MaxHeight – (MaxHeight – MinHeight) * CurrentItr / TotalItr;
这样第一次迭代所产生的影响最大,而后的迭代中对整个高度图的影响依次递减,在之前的高度基础上继续增加高度图中的细节。通常来说,迭代次数越多,效果会越好。
 
实现FaultFormation的伪码如下:
 
Do_Formation(float* pfBuffer, int iLen, Point ptStart, Point ptEnd, float fDelta)
{
         Point ptDir = ptEnd – ptStart;
 
         For (int z = 0; z < iLen; ++z)
                   For (int x = 0; x < iLen; ++x)
                   {
                            Point ptDirCur [...]