这篇文章是Gamer和意念在讨论“抽象能达到什么程度”这个问题时的一些重点的整理记录。意念曾希望能做出一种系统,这个引擎的每一个模块都可以单独拿下来使用,这是我们这次争论的焦点。好在最终我们终于取得了共识:“对系统而言,不变性的意义高于可变性”。确实,一个系统的原子部分,例如文件、Codec等等这些小系统都是可以拆散的,任何引擎都可以,不是哪个引擎的专利,但是,当牵扯到几个大系统之间的关系的时候,这就是比较麻烦的事情了,强制拆散可能会导致更加恐怖的结果。因此,在不变性的前提下谈可变性,才让一切有了根据。
------------------------------------------------------------------------------------------------------------------------
最近意念向我推荐了一个引擎,名字叫做Irrlicht,看他那样子好像恨不得一口就把这个引擎全吞下去
。我有点颇不以为然,因为对我而言引擎这种东西,不是自己写的,不是工作用的,大凡也就是三分钟热度,多也不过就是了解了解构架就Over了。加之正在看那厮前一段时间推荐的OGRE,觉得一个引擎再过度设计,又能达到什么地步呢?达到OGRE这样已经算是到了“真理再走一步就是谬误”的地步,所以觉得大可以不必再看别的。
去SourceForge下了Irrlicht后,才发现作者是如此BT,类如OGRE那样把引擎层细分作抽象引擎层和具象引擎层还不够,还要把抽象层全写成接口类,搞出了一个极为抽象的接口层。这些接口层基本按照下面的方式分割封装:Base、Video(Render)、IO(File)、Core(Math和Physics)、Scene、GUI。这从根本上动摇了我从OGRE那里继承过来的一种根深蒂固的概念:引擎要“自我完整”。对于Irrlicht,你完全可以把它分割为两个工程:“抽象接口”工程和“具象实现”工程,两者只有接口联系,甚至你可以用一个具象层实现IO和Scene,另一个实现Video和Scene(Base必须是抽象本身就要实现的,这中间包括仿照COM做的IUnkown接口),只要你严格按照抽象接口的标准就在原理上可以。这样做有必要吗?
软件开发需要遵循的就是一种标准,标准的意义在哲学上就是遵循一定程度的“不变性”,例如我们总是能确定我们在文件IO的时候,总是可以调用CPPSTD或者CSTD的函数,我们总是能确定我们在开发的时候,使用DX或者OpenGL的API。只有在不变性的基础上谈可变性,才有谈论的可能和必要。OGRE是Gamer接触的第一个融合了不变和可变的引擎,在此之前,Gamer所看过的只有不变性甚强的《圣剑2》引擎。
前不久意念兄说了一句真实的废话,“OGRE的可变性遵循的是接口不变性”
。在C++中寻找可变,如果不遵循接口不变性,难道还有别的不变性可以遵循吗?接口不变性之外,OGRE在接口的实现方式上实现了很大程度上的可变性,这估计是很多人研究和借鉴它的原因。但是OGRE可能走得越来越远了些,您可以尝试着把OGRE的某一个模块剪切下来,供自己改造和使用,这时您可能会发现,OGRE的不变性似乎已经超出了接口不变性的范畴,在设计上,无法避免的各个模块关系,使得OGRE花出了一定功夫去解决这些问题,这些模块关系在设计的角度上,也许成为了一种新的不变性。如果您剪切下来OGRE的渲染模块:包括Renderable、RenderQueue、Material、Texture、RenderTarget等等所有与渲染这个概念相关的那些类,你会发现他们无法跟你业已设计好的其他系统作出最完美的融合。如果您需要一个无缝的、完美的融合,您只能求助于再把OGRE的其他系统:资源、数据、控制等等全部迁入,这时您又发现这些系统对于您而言,有可能仅仅是一个又一个的累赘。
在这种两难的选择面前,我们似乎看到了一些更深层次的东西:模块化的设计思路究竟应该怎样得到贯彻?这里对于每个系统都是有不同的设计,也各有各的好处,评定好坏是没有意义的,只能说,在某个问题上占优势,在某个问题上占劣势。
现在我们走一遍OGRE的路:OGRE将各个系统间的重要相互关系一起纳入到“不变性”的角度,在此之上实作出一个伟大的、完美的、数据驱动的、融合良好的系统,这个系统在接口上可以变更实现。但是,如前所述,这样一个系统,其重要相互关系由于已经处于了“不变性”的角度,崩离其中一环,就不得不将别的环也引入。虽然对于OGRE而言,这种不变性一般是没有人会去碰的,因为这种不变性立足的是我们现实所决定的一些不变性,例如图形API多也就是Dx和OpenGL的样子(管线因此而不变),文件操作大凡就是取数据到内存(DataChunk及被其所支持的Resource系统因此不变)。例如我们自己写的文件和资源系统,想替代OGRE的思路,但是,OGRE其他系统的设计思路是按照Resource-DataChunk这个方式来的,这种替代也是一种比较令人烦闷的事情, 因为Resource系统在OgreMain一级就已经确定了,因此,更改的时候,除非更改整个OGREMain。我们不希望像OGRE那样复杂的去管理Frame的更新速率,其实我们需要的很简单,而OGRE却相对复杂(慢)了很多,但我们最后只能悲叹, 因为Frame更新速率这些类是写在OgreMain里面的,也就是说我们除非重新组织OGREMain的相关类。我们为什么会遇到这样那样的问题?
因此我们得出的结论是,OGRE假定了这么多东西,虽然保证了一个稳固的基础核心结构,让一些破坏性质的东西无处下手,也让我们的一些需要只能按照OGRE的假设来行事。也就是说,虽然OGRE是支持模块化的,但是,却要按照OGRE的原则模块化,不能满天遍地胡思乱想去模块化。
从上面的论述我们可以看到,我们需要一个引擎,首先要保证其稳固性,因此才能在稳固性的前提下提出扩展性。这里稳固性的程度却是尚未定义的,可以做出点手脚。OGRE无疑手脚太麻利了,以至于妄图把所有的东西都纳入到自己的旗下,OGRE本身的稳固性勿庸置疑地完美,同时带来的坏消息就是当我们触及这些稳固性问题的时候,无从入手。可以这么说,在目前的技术状态下,一切引擎都具有这个问题,就像一切太阳都会东升西落。这就是稳固性和扩展性之间的一个小矛盾。
前面说了,一切引擎的、或者说一切构架的基本哲学原则就是在不变的基础上寻找可变(不变的地基和可变的房屋式样),不变和可变是引擎中的一对共生的矛盾,Irrlicht也不例外。可以说在这个意义上,Irrlicht与OGRE没有本质区别。
OGRE的抽象结构和一部分不准备抽象的具象结构全部平摊在OgreMain里面,一旦需要更改,那么将会是相当严重的事情。有没有什么方法能让一个引擎在抽象层上能尽可能被分割为互不相关的模块呢?按照前面的说法,这个似乎会破坏一个引擎赖以生存的完整性,直觉上讲,分割的系统由于存在分割的缝隙,完整性就受到了挑战。在渲染中,确实需要考虑到Texture文件,即便是在抽象意义上也不得不考虑这种联系,因此,如果为了分割模块而破坏这种关系,极有可能导致一系列不可预知的结果。我们注意到了Irr的一个特点,在高层完全抽象的结果给我们留下了很大的空间去发挥,例如我们在Render中可以尽量少考虑来自IO的ReadFile对象,而仅仅考虑文件名。在内部实现中,如果是希望引继IO的,可以用ReadFile来实现,如果不希望,那就用自己的方式来实现,因为Render给你的只是文件名,你就可以用很多自己需要的方式来搞定。尽管对于Irr并非完全按照这种方式组织所有系统的,但是凡是这样组织的系统无疑比按其他方式组织。这样,我们把不变性,从对象一级(File对象)压到了标识一级(文件名)。这样无疑就让对象一级的联系被打散了。只是在具象层,才会重组这样的联系。而在具象层,是利用Irr自带的IO还是你自己写的IO,这是你自己的问题。这样,在不隔离模块的前提下,采用一种抽象的方式破坏对象联系,转而采用标识联系,效果会好得多。但在最终,你无法避免两个系统之间的联系,这种不变性,你可以把它从抽象层放到具象层,但无论在哪一层,你都要去实现,这就意味着: 你必须在引擎之中实现。因此从引擎的角度来说,这是一个永远无法逃避的东西,只不过OGRE的做法更为绝对,而Irrlicht的做法更为温和。
--------------------------------------------
So,那种对把引擎抽象为模块集合,似乎是没有太大的意义,因为模块之间的关系你必须要考虑。当然你可以使用大量的设计模式来取得更大程度上的扩展性,但是,事实上,存在的联系是即便连设计模式也无法消除的。正确的做法, 是正视稳固性,正视决定论,正视不变性,在不变的前提下构架一个扩展的系统,就像OGRE和Irr做的那样,而不是一味批驳它们修改起来是多么多么麻烦。那么我想反问你一句,为何要修改它呢?你既然觉得不好,为何要使用这个引擎呢?
使用引擎是一种契约,契约本身就是要遵循约定,如果你不准备遵循它的约定,那么干脆就不要订立这个契约,老祖宗的还是对的:“道不同,不相与谋”么!!
------------------------------------------------------------------------------------------------------------------------
最近意念向我推荐了一个引擎,名字叫做Irrlicht,看他那样子好像恨不得一口就把这个引擎全吞下去
![](http://blog.csdn.net/Emoticons/tongue_smile.gif)
去SourceForge下了Irrlicht后,才发现作者是如此BT,类如OGRE那样把引擎层细分作抽象引擎层和具象引擎层还不够,还要把抽象层全写成接口类,搞出了一个极为抽象的接口层。这些接口层基本按照下面的方式分割封装:Base、Video(Render)、IO(File)、Core(Math和Physics)、Scene、GUI。这从根本上动摇了我从OGRE那里继承过来的一种根深蒂固的概念:引擎要“自我完整”。对于Irrlicht,你完全可以把它分割为两个工程:“抽象接口”工程和“具象实现”工程,两者只有接口联系,甚至你可以用一个具象层实现IO和Scene,另一个实现Video和Scene(Base必须是抽象本身就要实现的,这中间包括仿照COM做的IUnkown接口),只要你严格按照抽象接口的标准就在原理上可以。这样做有必要吗?
软件开发需要遵循的就是一种标准,标准的意义在哲学上就是遵循一定程度的“不变性”,例如我们总是能确定我们在文件IO的时候,总是可以调用CPPSTD或者CSTD的函数,我们总是能确定我们在开发的时候,使用DX或者OpenGL的API。只有在不变性的基础上谈可变性,才有谈论的可能和必要。OGRE是Gamer接触的第一个融合了不变和可变的引擎,在此之前,Gamer所看过的只有不变性甚强的《圣剑2》引擎。
前不久意念兄说了一句真实的废话,“OGRE的可变性遵循的是接口不变性”
![](http://blog.csdn.net/Emoticons/tongue_smile.gif)
在这种两难的选择面前,我们似乎看到了一些更深层次的东西:模块化的设计思路究竟应该怎样得到贯彻?这里对于每个系统都是有不同的设计,也各有各的好处,评定好坏是没有意义的,只能说,在某个问题上占优势,在某个问题上占劣势。
现在我们走一遍OGRE的路:OGRE将各个系统间的重要相互关系一起纳入到“不变性”的角度,在此之上实作出一个伟大的、完美的、数据驱动的、融合良好的系统,这个系统在接口上可以变更实现。但是,如前所述,这样一个系统,其重要相互关系由于已经处于了“不变性”的角度,崩离其中一环,就不得不将别的环也引入。虽然对于OGRE而言,这种不变性一般是没有人会去碰的,因为这种不变性立足的是我们现实所决定的一些不变性,例如图形API多也就是Dx和OpenGL的样子(管线因此而不变),文件操作大凡就是取数据到内存(DataChunk及被其所支持的Resource系统因此不变)。例如我们自己写的文件和资源系统,想替代OGRE的思路,但是,OGRE其他系统的设计思路是按照Resource-DataChunk这个方式来的,这种替代也是一种比较令人烦闷的事情, 因为Resource系统在OgreMain一级就已经确定了,因此,更改的时候,除非更改整个OGREMain。我们不希望像OGRE那样复杂的去管理Frame的更新速率,其实我们需要的很简单,而OGRE却相对复杂(慢)了很多,但我们最后只能悲叹, 因为Frame更新速率这些类是写在OgreMain里面的,也就是说我们除非重新组织OGREMain的相关类。我们为什么会遇到这样那样的问题?
因此我们得出的结论是,OGRE假定了这么多东西,虽然保证了一个稳固的基础核心结构,让一些破坏性质的东西无处下手,也让我们的一些需要只能按照OGRE的假设来行事。也就是说,虽然OGRE是支持模块化的,但是,却要按照OGRE的原则模块化,不能满天遍地胡思乱想去模块化。
从上面的论述我们可以看到,我们需要一个引擎,首先要保证其稳固性,因此才能在稳固性的前提下提出扩展性。这里稳固性的程度却是尚未定义的,可以做出点手脚。OGRE无疑手脚太麻利了,以至于妄图把所有的东西都纳入到自己的旗下,OGRE本身的稳固性勿庸置疑地完美,同时带来的坏消息就是当我们触及这些稳固性问题的时候,无从入手。可以这么说,在目前的技术状态下,一切引擎都具有这个问题,就像一切太阳都会东升西落。这就是稳固性和扩展性之间的一个小矛盾。
前面说了,一切引擎的、或者说一切构架的基本哲学原则就是在不变的基础上寻找可变(不变的地基和可变的房屋式样),不变和可变是引擎中的一对共生的矛盾,Irrlicht也不例外。可以说在这个意义上,Irrlicht与OGRE没有本质区别。
OGRE的抽象结构和一部分不准备抽象的具象结构全部平摊在OgreMain里面,一旦需要更改,那么将会是相当严重的事情。有没有什么方法能让一个引擎在抽象层上能尽可能被分割为互不相关的模块呢?按照前面的说法,这个似乎会破坏一个引擎赖以生存的完整性,直觉上讲,分割的系统由于存在分割的缝隙,完整性就受到了挑战。在渲染中,确实需要考虑到Texture文件,即便是在抽象意义上也不得不考虑这种联系,因此,如果为了分割模块而破坏这种关系,极有可能导致一系列不可预知的结果。我们注意到了Irr的一个特点,在高层完全抽象的结果给我们留下了很大的空间去发挥,例如我们在Render中可以尽量少考虑来自IO的ReadFile对象,而仅仅考虑文件名。在内部实现中,如果是希望引继IO的,可以用ReadFile来实现,如果不希望,那就用自己的方式来实现,因为Render给你的只是文件名,你就可以用很多自己需要的方式来搞定。尽管对于Irr并非完全按照这种方式组织所有系统的,但是凡是这样组织的系统无疑比按其他方式组织。这样,我们把不变性,从对象一级(File对象)压到了标识一级(文件名)。这样无疑就让对象一级的联系被打散了。只是在具象层,才会重组这样的联系。而在具象层,是利用Irr自带的IO还是你自己写的IO,这是你自己的问题。这样,在不隔离模块的前提下,采用一种抽象的方式破坏对象联系,转而采用标识联系,效果会好得多。但在最终,你无法避免两个系统之间的联系,这种不变性,你可以把它从抽象层放到具象层,但无论在哪一层,你都要去实现,这就意味着: 你必须在引擎之中实现。因此从引擎的角度来说,这是一个永远无法逃避的东西,只不过OGRE的做法更为绝对,而Irrlicht的做法更为温和。
--------------------------------------------
So,那种对把引擎抽象为模块集合,似乎是没有太大的意义,因为模块之间的关系你必须要考虑。当然你可以使用大量的设计模式来取得更大程度上的扩展性,但是,事实上,存在的联系是即便连设计模式也无法消除的。正确的做法, 是正视稳固性,正视决定论,正视不变性,在不变的前提下构架一个扩展的系统,就像OGRE和Irr做的那样,而不是一味批驳它们修改起来是多么多么麻烦。那么我想反问你一句,为何要修改它呢?你既然觉得不好,为何要使用这个引擎呢?
使用引擎是一种契约,契约本身就是要遵循约定,如果你不准备遵循它的约定,那么干脆就不要订立这个契约,老祖宗的还是对的:“道不同,不相与谋”么!!