文/tanhongsong 原文地址:http://www.wantgame.net/blog/?p=794

意圖

用基類提供的操作集合,去定義子類的行為。

動機(jī)

每個(gè)孩子都有一個(gè)當(dāng)超級英雄的夢,但是地球上并沒有那么多宇宙射線(可以讓人變異的射線)。但是,游戲能讓你成為虛擬的英雄。因?yàn)樵谖覀冇螒蛟O(shè)計(jì)師的字典里,沒有”不行”,我們的目標(biāo)是為超級英雄提供幾十甚至上百種超能力,供英雄們選擇。

我們的計(jì)劃是先定義一個(gè)Superpower基類。然后,我們將會有子類去繼承它,每一個(gè)子類實(shí)現(xiàn)一種超能力。我們會寫一個(gè)設(shè)計(jì)文檔,分發(fā)給團(tuán)隊(duì)中的程序員去編碼。完成后,我們會的到100個(gè)超能力類。

我們希望為玩家提供一個(gè)充滿變化的虛擬世界。能夠在游戲中體驗(yàn)到所有他兒時(shí)夢想的超能力。這就意味著我們的超能力子類有可能做任何事:播放聲音,生成特效,與AI交互,創(chuàng)建和銷毀游戲元素,物理模擬力。無所不包。

想象一下,如果我們召集團(tuán)隊(duì),讓他們?nèi)懩切┏芰Φ念悾瑫l(fā)生什么事情?

1、會產(chǎn)生大量的廢代碼。雖然不同的超能力有很多變化,但是也有很多相同的地方。他們很多會用相同的方式產(chǎn)生視覺效果,播放聲音。冷凍射線,傷害射線,第戎芥末射線實(shí)現(xiàn)起來都差不多。如果不是協(xié)同開發(fā),一定會有很多重復(fù)的代碼和努力。

2、游戲引擎中的任何部分都有可能能這些類耦合起來。如果不加深刻理解,程序員很容易在這些超能力類中,調(diào)用那些本不打算暴露出來的接口。就像渲染器可以很好的抽象成基層優(yōu)雅的結(jié)構(gòu),供外部調(diào)用,但是我敢打賭,這種結(jié)構(gòu)會被那些到處都是的超能力代碼終結(jié)掉。

3、當(dāng)這些外部系統(tǒng)需要改動時(shí),很可能會導(dǎo)致超能力部分的代碼被破壞。一旦我們的超能力類們相互耦合,或者跟游戲引擎中的一些部分耦合在了一起,這必然會導(dǎo)致牽一發(fā)動全身。這會讓人很糟心,因?yàn)閳D形、音頻、UI程序員并不能兼任玩法程序員。

4、很難定義所有超能力類都遵循的規(guī)則。比如我們希望所有的超能力發(fā)出的聲音都能按照優(yōu)先級排好隊(duì)。但如果我們上百個(gè)類都直接調(diào)用聲音引擎,就很難做到。

我們希望給玩法程序員提供一系列方法。你的超能力類想播放聲音?給你playSound()函數(shù)。你像要粒子系統(tǒng)?給你spawnParticles()。我們希望提供的這些操作能夠覆蓋你所有的需求,這樣你就不需要#include一堆亂七八糟的頭文件,也不需要關(guān)心代碼庫中的其他代碼。

我們可以通過在Superpower基類中定義protected方法來實(shí)現(xiàn)。把它們放在基類里,可以方便子類直接調(diào)用。做成protected方法(也可以是非虛函數(shù)),也保證了只有子類能夠調(diào)用。

1、創(chuàng)建一個(gè)新類,繼承Superpower。

2、覆蓋activate()沙箱方法。

3、實(shí)現(xiàn)這個(gè)方法,調(diào)用Superpower提供的protected方法。

我們可以解決代碼重復(fù)的問題,也就是盡可能提供高層級的通用方法。當(dāng)我們發(fā)現(xiàn)一些相同的代碼散落在不同的子類中時(shí),就可以把他們收集到Superpower中,然后提供一個(gè)新的方法。

我們解決了耦合性問題,就是讓他們耦合到同一個(gè)地方。Superpower本身會跟不同的游戲系統(tǒng)耦合,但是成百的子類就不需要了。他們只需要跟他們的基類耦合就可以了。當(dāng)游戲系統(tǒng)改變時(shí),修改Superpower類是必須的,但它的子類們就不需要了。

這個(gè)模式會產(chǎn)生一個(gè)很淺、很廣的繼承結(jié)構(gòu)。繼承鏈不深,但是會有大量的子類繼承自Superpower。通過這個(gè)具有很多子類的基類,我們就得到了一個(gè)代碼上的杠桿指點(diǎn)。我們在Superpower中賦予的時(shí)間和愛,會讓所有的子類受益。

模式

有一個(gè)基類,定義了一個(gè)抽象的沙箱方法,并且提供了很多工具函數(shù)。把這些函數(shù)定義為protected類型,以保證它只被子類調(diào)用。每一個(gè)繼承這個(gè)基類的沙箱子類,都用它提供的工具函數(shù)實(shí)現(xiàn)這些沙箱方法。

何時(shí)用

Subclass Sandbox模式非常簡單。不像那些常見的模式需要大量的代碼,有的甚至在游戲外也需要很多代碼(這里可能指bytecode,需要編譯器)。如果你代碼中有protected類型的方法,你就很有可能已經(jīng)用類似的模式了。Subclass Sandbox模式比較適合以下幾種情況:

1、你的基類有很多子類。

2、基類可以提供子類使用的所有函數(shù)。

3、子類中有很多重復(fù)的代碼,你希望更容易地復(fù)用這些代碼。

4、你希望在子類和其他部分的程序之間去耦合。

牢記

“繼承”這個(gè)詞在編程界已經(jīng)臭大街了,其中一個(gè)原因就是基類會變得越來越臃腫。而這個(gè)模式恰好很容易導(dǎo)致這種情況。

因?yàn)樽宇愂峭ㄟ^基類訪問游戲中的其他系統(tǒng)的,基類就自然而然的要跟子類要訪問的系統(tǒng)耦合到一起。當(dāng)然,子類也跟基類緊密地耦合在一起,像這樣蜘蛛網(wǎng)一樣的耦合關(guān)系導(dǎo)致基類很難改動——即玻璃基類問題

凡事有好的一面,那就是耦合性都推給了基類,這樣子類就可以被從其他游戲系統(tǒng)中分離出來。大多數(shù)行為就可以定義在子類中。這就意味著你大多數(shù)的代碼變得更整潔,更容易維護(hù)了。

如果你發(fā)現(xiàn)這個(gè)模式是把你的基類往火坑里推,也可以考慮拉回來點(diǎn)。把一些工具函數(shù)分散到不同的類里面去,Component模式可能會幫到你。

示例代碼

因?yàn)檫@是一個(gè)很簡單的模式,沒有太多的示例代碼。也不能說沒用——這個(gè)模式討論的是意圖,而不是實(shí)現(xiàn)的復(fù)雜度。

讓我們從Superpower基類開始:
?

  1. <blockquote>class Superpower

復(fù)制代碼
這個(gè)activate()方法是沙箱方法。由于是純虛函數(shù),子類必須重寫它。這就讓超能力子類的創(chuàng)建工作變得很清晰了。

那幾個(gè)protected方法,move(),playSound()和spawnParticles(),是提供的工具函數(shù)。子類實(shí)現(xiàn)activate()方法需要用到它們。

我們在這里就不實(shí)現(xiàn)那些工具函數(shù)了,但是在真實(shí)的游戲中那就是實(shí)實(shí)在在的代碼。這些代碼就是Superpower類從其他游戲系統(tǒng)中調(diào)用功能——move()可能會調(diào)用物理代碼,playSound()會調(diào)用聲音引擎,等等等等。由于這些都是在基類中實(shí)現(xiàn)的,這會讓Superpower類保持自封裝。

好了,現(xiàn)在讓我們創(chuàng)建一種超能力:

?

?

  1. class SkyLaunch : public Superpower
  2. {
  3. protected:
  4. virtual void activate()
  5. {
  6. // Spring into the air.
  7. playSound(SOUND_SPROING, 1.0f);
  8. spawnParticles(PARTICLE_DUST, 10);
  9. move(0, 0, 20);
  10. }
  11. };
復(fù)制代碼
此能力把超級英雄彈到空中,播放一個(gè)悅耳的音效,然后泛起一股煙塵。如果所有的超能力都類似——只是播放聲音,生成粒子系統(tǒng),和移動——那就根本沒必要用這個(gè)模式。相反,我們可以使用一個(gè)activite()的固定實(shí)現(xiàn),只使用聲音id,粒子系統(tǒng)類型,和移動作為參數(shù)。但是這適應(yīng)于每一個(gè)超能力都是相同的類型,只是數(shù)據(jù)有差異。再加工一下:

?

  1. class Superpower
  2. {
  3. protected:
  4. double getHeroX()
  5. {
  6. // Code here...
  7. }
  8. ?
  9. double getHeroY()
  10. {
  11. // Code here...
  12. }
  13. ?
  14. double getHeroZ()
  15. {
  16. // Code here...
  17. }
  18. ?
  19. // Existing stuff...
  20. }
復(fù)制代碼
這里我們添加幾個(gè)關(guān)于英雄位置的方法,我們的SkyLaunch子類可以使用它們:

?

?

  1. class SkyLaunch : public Superpower
  2. {
  3. protected:
  4. virtual void activate()
  5. {
  6. if (getHeroZ() == 0)
  7. {
  8. // On the ground, so spring into the air.
  9. playSound(SOUND_SPROING, 1.0f);
  10. spawnParticles(PARTICLE_DUST, 10);
  11. move(0, 0, 20);
  12. }
  13. else if (getHeroZ() < 10.0f)
  14. {
  15. // Near the ground, so do a double jump.
  16. playSound(SOUND_SWOOP, 1.0f);
  17. move(0, 0, getHeroZ() - 20);
  18. }
  19. else
  20. {
  21. // Way up in the air, so do a dive attack.
  22. playSound(SOUND_DIVE, 0.7f);
  23. spawnParticles(PARTICLE_SPARKLES, 1);
  24. move(0, 0, -getHeroZ());
  25. }
  26. }
  27. };
復(fù)制代碼
因?yàn)槲覀兡茉L問一些狀態(tài)了,我們就可以做一些實(shí)實(shí)在在的有趣的控制流程。在這里只有幾個(gè)簡單的if語句,但是其實(shí)你可以做任何事情。通過這個(gè)充滿了寫死的代碼的沙箱方法,英雄救上天了。

設(shè)計(jì)抉擇

如你所見,Subclass Sandbox模式是一個(gè)可塑性很強(qiáng)的模式。它只是描述了一個(gè)基本思路,而沒有太多細(xì)節(jié)。這就意味著每次你用到它的時(shí)候,需要做出很多有意思的選擇。這里有幾個(gè)問題需要考慮:

提供什么樣的操作?

這是最大的問題。直接影響到這個(gè)模式的工作狀況和使用體驗(yàn)。最極限的情況,是在基類中不提供任何操作。它只有沙箱方法。要實(shí)現(xiàn)這個(gè)沙箱方法,你就不得不調(diào)用基類以外的游戲系統(tǒng)。如果你接受了這個(gè)方案,那就相當(dāng)于沒用這個(gè)模式(廢話)。

另一種極限情況,是基類提供了子類所需的所有操作,子類只與基類耦合,不調(diào)用任何基類以外的代碼。

在這兩種極限情況之間,有著大量的中間地帶。哪些需要基類提供操作?哪些需要子類直接調(diào)用外部代碼?你提供的操作越多,子類與外部系統(tǒng)的耦合度就越低,但是基類本身耦合度就越高。只是把耦合度從子類身上抽離,放在了基類身上。

如果你有很多與外界耦合的子類。通過吧這些耦合轉(zhuǎn)移到基類提供的操作中,也就是集中在了基類里。但這樣的事情做的越多,基類就變得越來越大,而且越難維護(hù)。

那么我們要在哪里劃線?這里有幾個(gè)規(guī)則可以參考:

如果一個(gè)操作只被一個(gè)或者少數(shù)的幾個(gè)子類用到,那你就沒得到太多收益。在基類中徒增復(fù)雜度,影響到了所有類,但只有少數(shù)子類獲益。

如果是為了保持操作的一致性,或者讓一些子類對外部系統(tǒng)的特殊訪問更加簡潔,那還是有價(jià)值的。

如果調(diào)用到游戲中的不改變?nèi)魏螤顟B(tài)的代碼,那就比較安全。雖然也有耦合性,但這種耦合性是安全的。如果調(diào)用到了一些改變狀態(tài)的代碼,你就需要格外小心。把他們放進(jìn)基類中,就會得到更好的安全性。

如果基類提供的操作知識訪問了外部的代碼,那它就沒有太大價(jià)值。這種情況下,直接調(diào)用反而更容易。

然而,即使是簡單的封裝,也是有用的——這些方法通常都是訪問那些基類不想直接暴露給子類的狀態(tài)。例如,Superpower提供像這樣的操作:

?

?

  1. void playSound(SoundId sound, double volume)
  2. {
  3. soundEngine_.play(sound, volume);
  4. }
復(fù)制代碼
這只是轉(zhuǎn)調(diào)了Superpower的成員 soundEngine_,好處是,這樣保證了這個(gè)成員封裝在了Superpower中,不被它的子類訪問。

是直接提供操作,還是通過包含的對象提供?

這個(gè)模式的一個(gè)巨大的挑戰(zhàn)就是,你最終要在基類中維護(hù)大量的方法。你可以把它們轉(zhuǎn)移到其他類中,以減輕這種痛苦。這樣,基類提供的方法只需要返回一個(gè)包含這些方法的對象。

例如,讓一個(gè)超能力播放聲音,我們可以直接在Superpower中添加:

?

?

  1. class Superpower
  2. {
  3. protected:
  4. void playSound(SoundId sound, double volume)
  5. {
  6. // Code here...
  7. }
  8. ?
  9. void stopSound(SoundId sound)
  10. {
  11. // Code here...
  12. }
  13. ?
  14. void setVolume(SoundId sound)
  15. {
  16. // Code here...
  17. }
  18. ?
  19. // Sandbox method and other operations...
  20. };
復(fù)制代碼
但如果Superpower已經(jīng)很臃腫,我們就可以避免。我們可以創(chuàng)建一個(gè)SoundPlayer類,去提供這樣的功能:

?

?

  1. class SoundPlayer
  2. {
  3. void playSound(SoundId sound, double volume)
  4. {
  5. // Code here...
  6. }
  7. ?
  8. void stopSound(SoundId sound)
  9. {
  10. // Code here...
  11. }
  12. ?
  13. void setVolume(SoundId sound)
  14. {
  15. // Code here...
  16. }
  17. };
復(fù)制代碼
然后Superpower提供訪問它的方法:

?

?

  1. class Superpower
  2. {
  3. protected:
  4. SoundPlayer getSoundPlayer()
  5. {
  6. return soundPlayer_;
  7. }
  8. ?
  9. // Sandbox method and other operations...
  10. ?
  11. private:
  12. SoundPlayer soundPlayer_;
  13. };
復(fù)制代碼
把操作放到分散的類中,可以為你帶來這些好處:

它可以減少你基類中的方法,在這個(gè)例子中,我們把3個(gè)方法編程了一個(gè)getter方法。

這些工具類中的代碼更容易維護(hù)。核心的基類(像Superpower),很難被改動,因?yàn)樘嗟念愐蕾囁?。如果把這些功能函數(shù)轉(zhuǎn)移到耦合性更低的第二個(gè)類中,我們就可以很容易改動這些代碼而不破壞其他東西。

它降低了基類和其他游戲系統(tǒng)的耦合性。當(dāng)Superpower直接提供playSound()時(shí),我們的基類就牢牢地同SoundId和聲音相關(guān)的實(shí)現(xiàn)代碼綁在了一起。把這些方法轉(zhuǎn)移到SoundPlayer中,就是把Superpower的耦合性轉(zhuǎn)移到了SoundPlayer類中,在這個(gè)類中就可以封裝所有相關(guān)的依賴代碼。

基類如何獲得他需要的數(shù)據(jù)?

基類通常需要封裝一些對子類不可見的數(shù)據(jù)。在我們第一個(gè)例子中,Superpower類提供了一個(gè)spawnParticles()方法。如果它實(shí)現(xiàn)的時(shí)候需要一些粒子系統(tǒng)對象,如何獲得呢?

1、傳遞給基類的構(gòu)造函數(shù):

最簡單的方案就是把它作為基類構(gòu)造函數(shù)的參數(shù):

?

?

  1. class Superpower
  2. {
  3. public:
  4. Superpower(ParticleSystem* particles)
  5. : particles_(particles)
  6. {}
  7. ?
  8. // Sandbox method and other operations...
  9. ?
  10. private:
  11. ParticleSystem* particles_;
  12. };
復(fù)制代碼
這就可以確保每一個(gè)Superpower在創(chuàng)建的時(shí)候就會擁有粒子系統(tǒng),但是讓我們看一下子類:

?

?

  1. class SkyLaunch : public Superpower
  2. {
  3. public:
  4. SkyLaunch(ParticleSystem* particles)
  5. : Superpower(particles)
  6. {}
  7. };
復(fù)制代碼
問題出現(xiàn)了,每一個(gè)子類都需要在構(gòu)造的時(shí)候調(diào)用基類的構(gòu)造函數(shù),并傳遞參數(shù)。這就把一些不希望子類知道的數(shù)據(jù)暴露給子類了。

這維護(hù)起來也很讓人頭痛。如果我們想在基類中添加一個(gè)狀態(tài),就需要該所有的子類去傳遞給它。

兩層初始化

為了避免通過構(gòu)造函數(shù)傳遞所有數(shù)據(jù),我們可以把初始化工作分成兩部分。構(gòu)造函數(shù)不設(shè)參數(shù),只用來創(chuàng)建對象。然后我們調(diào)用另一個(gè)基類定義的方法,去把其他需要的數(shù)據(jù)傳遞給它:

?

?

  1. Superpower* power = new SkyLaunch();
  2. power->init(particles);
復(fù)制代碼
注意因?yàn)槲覀儧]有在SkyLaunch構(gòu)造函數(shù)中傳遞任何東西,所以它并沒有跟Superpower的私有數(shù)據(jù)產(chǎn)生任何耦合性。這種方式的麻煩在于你必須確保記著調(diào)用init(),如果你忘了,你就會得到一個(gè)初始化一半的對象,它就可能不好用。

你可以解決這個(gè)問題,把整個(gè)過程封裝在一個(gè)單獨(dú)的函數(shù)里,就像這樣:

?

  1. Superpower* createSkyLaunch(ParticleSystem* particles)
  2. {
  3. Superpower* power = new SkyLaunch();
  4. power->init(particles);
  5. return power;
  6. }
復(fù)制代碼
使用靜態(tài)成員:

在前面的例子中,每一個(gè)Superpower實(shí)例的初始化,我們都用了一個(gè)單獨(dú)的粒子系統(tǒng)。這是假定每一個(gè)超能力都需要一份單獨(dú)的數(shù)據(jù)。但是我們的粒子系統(tǒng)是一個(gè)Singleton,每個(gè)超能力類都用的同一個(gè)粒子系統(tǒng)。

這樣,我們就可以把這個(gè)數(shù)據(jù)變成基類的私有成員,并且設(shè)置為static類型。游戲中仍然需要確保這個(gè)成員被初始化,但是只需要在Superpower類中初始化一次,而不是每一個(gè)對象都要初始化。

?

?

  1. class Superpower
  2. {
  3. public:
  4. static void init(ParticleSystem* particles)
  5. {
  6. particles_ = particles;
  7. }
  8. ?
  9. // Sandbox method and other operations...
  10. ?
  11. private:
  12. static ParticleSystem* particles_;
  13. };
復(fù)制代碼
這里注意init()和particles_都是靜態(tài)的。只要在開始的時(shí)候調(diào)用一次Superpower::init()一次,每一個(gè)超能力就可以訪問例子系統(tǒng)了。同時(shí),Superpower對象也可以通過子類的構(gòu)造函數(shù)隨意創(chuàng)建了。

還有一個(gè)好處就是particle_是一個(gè)靜態(tài)變量,我們不需要為每一個(gè)Superpower對象存儲一份,節(jié)約了內(nèi)存。

使用service locator:

前面的選項(xiàng)都需要外部代碼創(chuàng)建這些成員,然后在基類使用它們之前,設(shè)置到基類中。這會把一些初始化代碼寫的到處都是。另一個(gè)辦法是讓數(shù)據(jù)自己創(chuàng)建,基類使用的時(shí)候直接拉過來用。其中一種方式就是用Service Locator模式:

?

?

  1. class Superpower
  2. {
  3. protected:
  4. void spawnParticles(ParticleType type, int count)
  5. {
  6. ParticleSystem particles = Locator::getParticles();
  7. particles.spawn(type, count);
  8. }
  9. ?
  10. // Sandbox method and other operations...
  11. };
復(fù)制代碼
這里,spawnParticles()需要一個(gè)粒子系統(tǒng),不再需要外部代碼送進(jìn)來,它可以從service locator中自己取。

銳亞教育

銳亞教育,游戲開發(fā)論壇|游戲制作人|游戲策劃|游戲開發(fā)|獨(dú)立游戲|游戲產(chǎn)業(yè)|游戲研發(fā)|游戲運(yùn)營| unity|unity3d|unity3d官網(wǎng)|unity3d 教程|金融帝國3|8k8k8k|mcafee8.5i|游戲蠻牛|蠻牛 unity|蠻牛