毫无疑问,移植工作本身并不是一个真正的挑战,因为问鼎的架构从设计开始就为移植作了充分的准备。问鼎采用的来自第三方的模块都经过了仔细的挑选,包括 - CEGUI、OGRE、freetype、iconv、mysql client、sqlite、gloox、berkeley db、tinyxml、fmod - 都是可跨平台的;我们自己设计的代码C/C++模块也保持了高度的可移植性,所有操作系统相关的代码是剥离的;最后,主要游戏逻辑是基于LPC这样的脚本语言实现的,而其解释器本身已经在所有常见操作系统验证过了。
所以,我一直懒于进行这个力气活。直到前不久,我认为现阶段不会有闲人来做移植工作,于是我打算动手来解决这个问题。
因为CEGUI本身是可移植的,并且其demo包含了输入、输出这种与操作系统密切相关的实现,同时我们又修改了CEGUI非常多的代码,所以我开始从这个工程入手:先移植CEGUI及其demo。
CEGUI本身有Xcode的工程文件,所以移植只是一个毫无技术含量的力气活。不过gcc审美角度显然和msvc不太一样,很多新增、修改的代码引起了gcc的不满,基于gcc的Xcode报了上千处不满,我将用了差不多一周的时间它们一一剔除,才完成了编译和链接。浪费时间的主要原因是要将所有的第三方库都在MacOS上编译一次,某些工程的确不太好伺候,另外就是我对Xcode完全不熟(或者说是完全无知更恰当一些),寻找各个功能点耗费了大量的时间,好在集成环境本质上只是配置管理,把握住这一点,剩下的工作就是寻找想要的配置参数。
执行的时候因为路径问题,导致CEGUI不太愿意配合,我暂时没有理会这些问题,直接通过插入几条chdir指明相应的路径,让CEGUI先运行了起来。
当CEGUI完成以后,我打算一鼓作气解决问题,OGRE有for MAC的SDK,看上去不需要我费事,于是我为我们自己的模块逐个增加makefile,其中和操作系统有关的代码被我直接comment out,堵住了gcc的嘴,算是比较顺利的通过了编译部分。其中有一点让我倍加恶心,当我引用了多个namespace,而这些namespace有重名类定义的时候(比如常见的Rect、Point这些类),gcc对未指明namespace的类型会提出complain而不是采用某一个作为默认,为此,我修改了上百处类似的引用。
注:类似如下的代码
declare A::class1
declare B::class1
当你直接使用class1的时候,除非代码是在A或B的namespace内,否则gcc会很不愉快,即使你之前注明了using namespace A也无用。解决这个问题的一个方法是:显式的使用A::class1。
编译、链接完成以后,就是需要干正事的时候了 - 你总得为移植写点什么代码,毕竟窗口不会无缘无故的冒出来,消息也不会鬼鬼祟祟的被处理,软件领域只有01,没有雷锋。
本着偷懒的一贯原则,从sample中剥离出有用的东西再加工是我比较喜欢用的手段。我研究了一下CEGUI的demo,它采用了glut,嗯,这是一个跨平台的窗口库,很好,这种东西是我的首选,我总不想每次移植的时候都“写点什么”,能不写就不写才符合我的原则。
然而,这次我遭受到了打击,所谓“出来混,总是要还的”。用glut创建出窗口非常容易,但是当我把它们胡乱塞给OGRE以后,屏幕上一团漆黑,显然这才对的(期望偷偷摸摸的就成功那是入门级程序员的幻想)。不过我并没有急于去研究OGRE,因为我用的是SDK而不是自己编译出来的库,所以我无法调试它。我尝先让OGRE自己创建窗口,结果更糟糕,它直接崩溃了,而且是崩溃在OpenGL的函数中,这真是让人沮丧。因为这不在程序员所控制的领域之内,并不是靠基本功就可以度过的难关,我必须采用某种调试手段,针对第三方代码进行调试,糟糕的是,我并没有这种手段。
陈拓琳告诉我(他的一个优点就是能够知道什么情况下需要使用什么工具,并且知道工具在哪里),在MacOS下可以使用OpenGL Profiler调试OpenGL。我试验了一下这个具有奇丑无比图标的工具(图标是一个金属夹子夹着一串字母“OpenGL”,其实说它像是一个烧瓶更贴切一些,本来我以为这是MacOS下所能见到的最丑的图标,不过微软新出的Office 2008 for Mac看上去更能挑战我的审美观)。一开始我试图在某个执行OpenGL函数错误后停下来,我得手了,但是那里显然不是导致崩溃的原因。我改换了一下思路,在glDrawXXX这几个函数上设置了断点,我发现它们每次都执行成功了!并且Back buffer的确得到了更新,一直到最后一个图形绘制OK,并且成功的进行了Swap buffer。这让人有点困惑,不过继续跟踪的结果有所收获,第二帧绘制的时候就崩溃了。
我仔细考虑了一下,既然只能绘制一帧,那么似乎Frame buffer的使用有关(我看到有两个Frame buffer),即可能和窗口有点关系,我通过glut建立了一个窗口,让OGRE自己建立了一个窗口,所以我先comment out了通过glut建立窗口的代码,结果就不崩溃了,这样,我总算在MacOS看到了熟悉的登录界面。
那么,如何处理窗口消息呢?我发现可以从OGRE中取回它创建的MacOS窗口,这是一个Carbon的句柄,我在上面注册了消息处理函数,结果失败,我无法捕捉到我想要的消息(事实上我捕捉到了其中某几个),这让我很困惑。于是我去参考了一下OGRE的例子,它使用的是OIS - 某一个可以跨平台的输入系统,按照道理,这本应该是我的最爱,可是这时候我已经发现OGRE自己的例子在MacOS上跑得都有问题,其实它并不能优雅的处理窗口消息,和操作系统协作,它只是一个简单的具有破坏性的demo。
我试图改造例子就OK的思路已经破产了。
我痛下决心,抛弃了glut,我要坚定不移的使用Carbon。在此之前,我先下载并编译了OGRE,不调试它就解决问题目前看来是不可能的了。还好,OGRE编译起来要容易地多,因为发布者本身也发布了OGRE所需要的Dependencies,所以编译OGRE只是一个点击鼠标就可以完成的任务,只是需要的时间有一点久。
我仔细研究了一下OGRE针对MacOS的实现,发现它和Windows相比有所不同,基于Windows时我们可以创建一个窗口用作消息处理,然后让OGRE在这个窗口中创建一个子窗口即可;而基于MacOS时,我们创建窗口后,要将窗口中某个HIView控件的句柄传入OGRE,让OGRE在这个控件指明的范围内绘制,我一开始没有阅读代码,所以实际上是在走弯路,我试图让glut和它配合本身就行不通。OGRE这里的实现完全没打算和glut配合,如果我想使用glut,需要自行实现窗口相关的若干代码才可以,不能使用OGRE提供好的一些代码,这意味如果我使用glut,将来是不是节省工作不好说,现在就先要还债。
很好,那就横下一条心用Carbon吧。
为了了解Carbon,我通过Xcode创建了最简易的小工程,大概了解了Carbon的思路。它采用接受注册的消息回调来处理,消息本身由EventClass和EventId两段组成。而分发消息也是通过消息循环完成的,看起来和Windows比较类似,不同点主要有:
1. Carbon看样子并不能登记一个截获所有消息的handler,而windows的handler是收到所有消息的
2. Carbon有预定义的消息循环,Windows都要靠自己实现(这点有很严重的差别,后面有提到)
因此我遇到了几个问题:
1. 如果MouseDown被我登记的handler处理后没有继续往下一个handler传递的话,很多窗口行为就丧失了,比如拖动窗口移动、关闭、最小化。我可不打算自己来实现这些,所以我倾向于把MouseDown继续传递给Standard handler(MacOS自带的),结果就是它会进行Capture,让我登记的handler收不到MouseUp。这真让人疯狂,我只好选择不让HIView内(相当于Windows的client area)的MouseDown继续传递,在以外区域的MouseDown就让后续的handler看着办吧。
2. 我用ReceiveEvent、SendEventToTarget来模拟类似Windows的消息循环,结果无法点击应用程序的菜单,为了这个事情我纳了半天的闷,最后看到文档下面写得清清楚楚,用这种方法就会如此... 苹果的开发网站上给了一个解决方法,太复杂,我投降了,还是用预置的消息循环吧,我另外注册Timer来解决消息循环中调度VM、进行渲染等问题。(这样效率不够高,而且响应还不是最及时的)
消息输入完成以后,进入游戏倒是很顺利,没有出什么大问题。接下来我在尝试把CEGUI当作私有的Framework并入应用程序时又遇到了问题,启动时抱怨在/Library/Frameworks下没有找到CEGUI,见鬼,系统不在本地的Frameworks下寻找跑那么大老远去找干什么?为什么OGRE就没有这个毛病?苹果的开发网站上给了答案,在build CEGUI时,需要在Install directory中执明是@executable_path/../Frameworks即可,原先指明的是/Library/Framrworks。更换了这个设置以后build出来的CEGUI,再被应用程序所链接,应用程序就可以访问位于私有目录下的CEGUI了。
最后就是收尾工作,扫荡所有留下的临时代码(比如强行设置路径,指明某些配置),需要采用一些方案能够在多个系统之间兼容,制作发布脚本,这些都是程序员的本分,属于可控的领域内。
完成这一切工作以后,基于MacOS运行的版本终于可以发布了(当然还有许多不足,某些功能也有缺欠)。应该说,游戏的移植相对是比较容易的,因为GUI系统相关的代码是最不容易移植的,而游戏很少使用它。
最后总结,如果打算移植,需要做点什么?
1. 设计时就准备好移植,采用可移植的第三方库、封装操作系统的函数、抽取操作系统相关的实现
2. 移植时按照这个步骤:编译 -> 链接 -> 小Demo调试 -> 集成调试 -> 逐步完善。每一步我们都只考虑完成相关的任务就可以了,避免把问题弄复杂。
ps:在我移植帖子的时候,已经是2010年6月了,两年过去,项目组为了维护多个平台的代码花了很大的力气。这并不是说保持在多个项目之间的同步有多大的难处,而是我在这次移植时没有完成所有的工作。
正确的做法应该是开发整个中间键,用来提供所有操作系统相关的功能,不能在某些代码中嵌入操作系统相关代码,然后用#if控制编译分支。当初我认为代码实现的差不多了,就没有花力气整理中间键的架构。后来因为项目有一些新的需求,结果代码增加的越发混乱。
后来,项目组一度尝试用wxWidget来作为中间键,这显然不是一个好主意,因为wxWidget过于庞大笨重了,让它和Ogre这些同样是重量级的模块耦合显然是不合适的。而且我们根本不需要用wxWidget这么复杂的中间件,毕竟游戏只需要一些很简单的I/O功能就可以了。