這里的“游戲”我只討論一般的小游戲,這里的渲染器也特指游戲中負責所有(或者說大部分)渲染工作的那個對象。而我這里說所的“架構(gòu)”則是指如何安排這個 “渲染器”與游戲中其他對象(類)的關(guān)系,之所以要討論這種關(guān)系,是因為很多時候我們都需要改善游戲中邏輯部分和渲染部分之間的關(guān)系。如果這種關(guān)系藕荷度太高,最直接的問題就是導致代碼可擴展性不高。
渲染代碼往往依賴于開發(fā)所使用的開發(fā)包。假使你直接使用DirectX或者OpenGL,你的渲染代碼很有可能會直接涉及到各種DirectX或OpenGL中的概念。在代碼級別,就是會牽扯進很多跟開發(fā)包相關(guān)的類,函數(shù),結(jié)構(gòu)體等等之類。
也許你會對DirectX或OpenGL做二次封裝,開發(fā)出一些游戲引擎(或者只是單純的圖形引擎)。于是,現(xiàn)在的渲染代碼就可以只跟你的游戲引擎相關(guān)(代碼級別)。
無論如何,如果直接使用DirectX或OpenGL(一次封裝都沒做這是很爛的方式),當你移植游戲時你很有可能就會面臨從DirectX到 OpenGL代碼轉(zhuǎn)化的問題(反之亦然)。而使用游戲引擎呢?游戲引擎隔離了底層具體的開發(fā)包(DirectX或OpenGL),但是如果要換引擎呢?換引擎這樣的情況我認為是完全存在的,例如現(xiàn)在在Windows下開發(fā)了一個基于HGE的游戲,現(xiàn)在我想把這個游戲移植到Linux 下。而HGE是基于DirectX的,要移植到Linux下,你當然可以保持HGE的接口不變,而使用OpenGL重寫HGE。這里,你就需要重新開發(fā)一個引擎!這很浪費時間!所以你可能會使用另一個在Linux 下運行的引擎(跨平臺的引擎也可以),例如ClanLib,于是,這里就涉及到了“換引擎”的問題。
從上文我們可以看出,基于很多原因(不僅僅是要把代碼做移植,好的架構(gòu)還利于代碼的擴展,利于開發(fā)過程---特指編碼過程----的平穩(wěn)進行等等之類),游戲中渲染器的架構(gòu)需要得到關(guān)注。
接下來我將討論幾種不同的架構(gòu):
一.這也是最爛的方式,游戲中無論在什么地方,只要需要渲染了,就加渲染代碼。其結(jié)果就是各種與圖形開發(fā)包相關(guān)的東西鋪天蓋地。這樣的代碼根本沒任何幽雅性可討論。事實上,在很多游戲開發(fā)相關(guān)的書籍中早就提到過類似“將游戲邏輯和渲染分離開來”的觀點。
二.這是我使用了很多次的架構(gòu)方式,類圖為:

Game類是一個Manager類,Renderer類里包含了游戲中所有的渲染代碼,Monster是一個具體的怪物類,它沒有類似于Render或者 Draw之類的接口,這些接口都被放在Renderer類里,例如RenderMonster()。當游戲需要渲染這個Monster對象時,Game對象就調(diào)用Renderer類的RenderMonster接口。
于是,現(xiàn)在Monster類里(基于這種架構(gòu),其他所有的精靈類以及各種需要渲染的非精靈類)就沒有任何與圖形開發(fā)包相關(guān)的東西。而與圖形開發(fā)包相關(guān)的東西則都集中到了Renderer類里。當你要移植代碼時,只需要重寫Renderer類即可。
但是這種架構(gòu)還是有很多問題:
1.隨著游戲越做越大,游戲中需要得到渲染的物體就會越來越多。這樣,每次都給Renderer類添加一個RenderSomething之類的接口(注意,除了添加接口外很有可能還要添加一些渲染所需要的對象,例如Surface之類的)。Renderer類最終將成為一個非常巨大的類,這違背了面向?qū)ο笤O(shè)計的原則(巨類導致的最直接的壞處就是改代碼痛苦-----鼠標滾輪滾半天才找到目標代碼。just a joke。)
2.事實上,Renderer類的RenderSomething 之類的接口在被調(diào)用時還需要一些渲染參數(shù)。例如渲染一個精靈類的話,就需要知道要渲染哪一幀,以及渲染到哪里之類的信息。在某些時候,這些參數(shù)會讓這些接口的declaration 變的很惡心!(代碼要幽雅,先把格式寫好看點------當然你愿意去參加“混亂代碼大賽”的話倒可以朝這個方向發(fā)展,another joke)
三.我始終記得一句關(guān)于面向?qū)ο笤O(shè)計的話:“面向?qū)ο笫怯脕砟M世界的”(大致是這樣的)。這句話其實真的可以作為面向?qū)ο箝_發(fā)的一個指導性原則,它會讓你在架構(gòu)系統(tǒng)時變得很容易。因此,套用這句話的話,上面那種架構(gòu)方式就出現(xiàn)一些讓人覺得別扭的地方了。怪物對象為什么不能自己渲染自己?(難道一個人表現(xiàn)自己的能力都沒有?爛比喻!)按照那條原則,怪物類理所應(yīng)該擁有一個Render之類的接口!
于是,架構(gòu)二稍微做了些變化:

我們讓每個需要渲染的物體,這里是Monster,都有一個Render接口以及一個Renderer對象指針。在創(chuàng)建該精靈對象時,Game類把自己創(chuàng)建的Renderer對象指針傳給該精靈對象。該精靈對象再保存該指針到mRender成員。當渲染時,Game類不再直接調(diào)用Renderer對象里的 RenderSomething之類的接口,而是很自然地調(diào)用Monster的Render接口。然后Monster的Render接口再通過自己保存的 Renderer對象指針調(diào)用其RenderMonster接口。
這里,我們?yōu)榱俗屧O(shè)計更貼近真實世界,就多加了一條依賴關(guān)系。(這其實純碎是某些完美主義程序員的癖好-----for instance, me :D)
四.這可能是種被普遍使用的方式:
類圖為:

這里不再有Renderer類了,精靈也不再象方式三中的那種“虛假”的Render了。取而代之的是,精靈類(以及其他需要渲染對象)自己直接使用圖形引擎來渲染自己,自己也保存渲染所需要的信息,例如那個mImage。
這種方式的缺點在于:每個需要渲染的物體就都會與圖形開發(fā)包相關(guān)。當然如果代碼組織得當,在移植時也不是很費勁。但是它造成了代碼層次上的相關(guān)性,讓我覺得不是很好。(代碼相關(guān)性:例如要涉及到圖形引擎的類(例如Image ),還要包含一些圖形引擎頭文件,這樣一折騰就讓我覺得很不爽)
總結(jié)了以上四種方式,我在這里試著提出一種個人覺得更為好的架構(gòu)方式(因為還沒在實際的項目中實驗過,所以只能說“試著”)。
關(guān)于渲染所需要的渲染信息,其最基本的信息就是類似于Surface,Image之類的用來代表該物體外觀圖片的對象。又由于圖片資源的一些屬性,很有可能還會涉及到一些輔助加工資源的變量(例如對于一個精靈,某一幀對應(yīng)于圖片上的矩形區(qū)域)。
這里涉及的問題就變?yōu)椋阂粋€需要被渲染的物體,除了其渲染代碼與特定的圖形開發(fā)包相關(guān)外,其渲染信息數(shù)據(jù)也與圖形開發(fā)包相關(guān)。如何盡可能地把這些代碼與邏輯部分代碼分離?
于是我得到了如下的架構(gòu)形式:

每一個需要渲染的物體都擁有一個其自己的渲染器,然后每個特定的渲染器還有一個其特有的資源管理器(這里主要管理圖片資源-----事實上只是作為一種資源加工器而已)。
這種方式其實就是上面提到的方式二的改進版本,把方式二中的那個巨類分割開來。而為了更方便地進行渲染,Monster在創(chuàng)建MonsterRenderer對象時可以把自身作為參數(shù)傳給MonsterRenderer并讓其保存。這樣,又多了條依賴關(guān)系:

有時候,對于一個精靈而言,為了靈活地配置其部分屬性(這部分屬性如果硬編碼的話很有可能就是一些常量或者宏),我們還要為精靈增加一個屬性讀取器,它會從外部配置文件讀入屬性-----事實上這不是本文討論的范圍-----例如使用一種XML解析庫來從XML配置文件中讀取數(shù)據(jù),可以這樣設(shè)計:

如果精靈渲染信息數(shù)據(jù)也需要由外部文件配置,并且與那些屬性被放在同一個配置文件中(這可能不是種好的習慣),這樣,PropertiesReader和MonsterResMgr就會從同一個XML配置文件中讀取數(shù)據(jù)。我們可以這樣解決這個問題:

所有配置數(shù)據(jù)都由PropertiesReader讀取,Monster負責創(chuàng)建PropertiesReader對象,然后把該對象指針傳給 MonsterRenderer對象。事實上,因為MonsterRenderer對象保存有Monster對象指針,所以完全可以訪問到 PropertiesReader對象