建築師——由來已久的夢想
ActionScript 3的引入激起了人們對於架構及設計模式的興趣。從第1章我們可以知道,設計模式基本上就是一種用以解決開發問題的藍圖或模板。它們可為程式開發提供可重用架構。在程式設計業內的某些領域,設計模式是開發中必不可少的部分。但由於悖離了ActionScript語言的固有特性,導致用AS3實現的設計模式往往會使開發受阻。其中一個原因就在於AS3語言已被設計為以一種特定機制來運作,具體來說就是事件機制。在本章中,我們將要探討物件導向程式設計(OOP)的一些基本原則,我們一定要在開發中牢記這些原則。另外還將介紹一些程式設計風格與有效的設計模式,並且還將告訴你何時應該忽略這些華麗技巧。
12.1 OOP概念
就像我在第1章所提到的那樣,物件導向程式設計技術是一種圍繞著物件互動這種概念的軟體設計模型。就遊戲範疇而言,螢幕上的每個遊戲角色及其周圍的每個互動元素都是一個物件。它們都有要接受的指令以及彼此間要傳送的訊息。通過讓每種物件都負責其自有行為,程式設計就會變得更加模組化且更為靈活。從理論上來說,這個概念可能並非難於掌握。但在實踐中,如果我們沒有一定的規劃和預計,則它將很難實現。這時就需要用到設計模式了。使用一種可靠的軟體設計風格會使人們規劃應用程式的過程變得更簡單,因為這種模板已經將各方面因素都考慮得很周詳了。注意,這裡我所說的是應用程式。許多公認的企業級設計模式非常適用於建立那種執行特定任務的應用程式,比如像生產力應用程式、實用工具軟體以及設計軟體等。然而,設計模式卻並不一定能適合遊戲開發的要求,因為從人們的感覺上來說,遊戲更像是一種體驗,而非是那種行為固定且可預期的商業軟體。開發遊戲引擎的最佳方案可能根本不會遵循常規的設計模式,但它卻依然能夠相當優秀地完成任務。然而在使用OOP時,你最好能夠遵照一些基本原則,它們能使你的程式碼實現模組化並具有可擴充套件性。
12.1.1 封裝
OOP中最重要的一個概念就是封裝。簡單地來說,封裝指的就是物件(在ActionScript中就是指類)應該具有獨立性,能夠自我管理。物件不必知道其功能實現環境的全部細節,它應該具備一個規定好的功能列表(或者說介面)以使其他物件能夠命令它去執行某項操作。為了與外部物件實現訊息傳遞,它所傳送的訊息應該要被其他物件“偵聽”到。一個封裝良好的物件就相當於一臺蘇打水自動售貨機。它所有的內部工作狀況你一概不知,其全部功能也只濃縮為兩種:選貨按鈕和出貨口(“哐當”一聲,你買的貨品就被推送出來)。你沒必要知道機器的內部工作原理,有可能是幾個侏儒在裡面現場釀製並灌裝這些蘇打水,也可能裡面只是一連串的軟管而已。無所謂了,你所關心的只是能否從一個易於理解與使用的介面處獲取美味蘇打水而已。看看任何一個Flash內建類,你就會發現它們所遵循的模式與此相同。幫助文件中所列出的類資訊只有公共方法、屬性及事件。儘管在該層面下肯定還有更多的資訊沒有暴露出來,但我們不必知道它的全部細節。你在開發自己的遊戲類時也應如法炮製。
12.1.2 繼承
假設我們有兩個類:Chair類與Sofa類。這兩個類都有一些相似點,因為它們都是坐具,擁有坐具的一般特徵——重量、尺寸、腿數、可坐人數等。為了省時,我們不會在這兩個類中定義出全部這些特徵,而只建立一個名叫Furniture(傢俱)的類,然後把傢俱的共有特徵加入到Chair與Sofa中。然後我們就可以說Chair類與Sofa類通過成為(或者說是擴充套件)Furniture類而繼承了這些屬性。這就是繼承的概念。現實生活與虛擬世界中所有的物件都有著一定的層次等級,而物件導向式程式設計的效率能否實現最大化,其關鍵也就在於你能否認清物件間的關係及其共有特徵。以前我們要為Chair類與Sofa類各定義出一個屬性,而現在如果用了繼承,你就只需簡單地把該屬性定義在Furniture類上即可。當擴充套件一個類時,擴充套件出的新類就變成了子類,而原始的被擴充套件類現在則被稱為超類。在上面這個例子中,Furniture是超類,而Chair與Sofa則是它的子類。稍後我們還將介紹純繼承(即一個類只能擴充套件自某一個類)在實踐應用上的一些不足之處。
12.1.3 多型性
儘管這個詞聽上去像是某種在科幻小說中出現的災難,然而這裡談到的多型性則與之不同,它基本上是指我們可以用程式碼將一個類替代為另一個類,並且通過繼承得到的物件的某些行為或屬性可被改變或者說重寫(overridden)。ActionScript只支援一種基本型別的多型,也就是我們這裡要介紹的內容。拿上面所舉的繼承範例中的Chair類來說吧。假如我們通過擴充套件Chair類得到一個小孩所用的HighChair(高腳椅)類。與正常的Chair相比,HighChair中的某些椅子屬性在用法上或表現形式上都有所不同。我們可以重寫那些在HighChair類中用法特殊的屬性,而依然繼承那些用法相同的屬性。實際操作過程不會如此複雜,我會在以後用到它時加以介紹。
12.1.4 介面
物件導向程式設計的一個核心原則就是要將介面與實現分離開。介面就是一種含有公共方法與屬性及其資料型別的列表。而實現則是使用了介面的類,它用介面來定義可被其他類公開獲取的方法及屬性。起初你可能不太理解這個概念,所以我們還是通過一個例子來進行說明。請注意在該例中(以及在本書的剩餘部分中)按照慣例ActionScript中的介面名稱首字母是大寫字母I。
在講解繼承時所用的範例中,Chair類與Sofa類都是擴充套件自Furniture類。但如果你要引入另一件傢俱(比如說Table),你就會遇到麻煩。儘管它們全都是傢俱,然而用途卻極為不同。Table類不需要那些能讓人坐下的方法,而其他兩個類也不需要能在自身之上放置菜餚的方法。固然,理論上你可以通過建立一個完整的繼承結構來將Furniture類拆分為SeatingFurniture類、DisplayFurniture類以及SupportFurniture類等。但這樣做實在是個笨辦法。另外,對較大的繼承結構所作的任何改動都勢必會波及到子類,並會產生一些以前根本不存在的麻煩。而介面卻能很方便地解決這個問題。
要想支援這三個類各自的需求,你只需定義出不同介面即可。你可將介面拆分為如下型別。
IFurniture,包含move()方法。
ISeatedFurniture,包含sitDown()方法。
ILayingFurniture,包含layDown()方法。
ITableFurniture,包含setDishes()方法。
繼承只允許一個類直接繼承另一個類,而單獨一個類卻可以使用盡可能多的介面。Chair類可以實現IFurniture與ISeatedFurniture介面。Sofa類除了可以實現以上這兩個介面外,還可以包含ILayingFurniture介面,而Table類則可含有IFurniture與ITableFurniture介面。另外,由於介面還可以互相擴充套件,你也可以擴充套件第一個介面而得到後三個介面,這使得實現起來就更為簡單。因為我們現在已為不同的furniture用途定義好了幾種基本介面,所以接下來如果遇到某種特殊傢俱,你就可以按照需要來組合使用這些介面。
不要因為這種較抽象的概念讓你覺得有些難懂而憂心。在第14章我們將要建立一個完整的遊戲,那時你就會在實際應用中領會到這些概念的含義了。
12.2 遊戲開發中的實用OOP技巧
通過使用事件在物件間傳遞訊息,AS3預設能夠支援OOP及良好的封裝。據說AS3的事件模型類似於觀察者(observer)設計模式,但不管定位如何,它都只是這門語言的固有執行方式。你一定要記住:無論其他的設計模式有多麼優秀,如果偏離了這種事件模型,那麼它們勢必將改變這門語言的預設行為。圖12-1展示出在AS3顯示列表層級中物件彼此間的關係。
在該示意圖中,Object1位於層級頂部,它可以是一個根顯示物件或者只是一個普通的資料事件分發器(EventDispatcher)。它有一個指向Object2的引用,且知道Object2的資料型別,故而可以直接通過公共介面傳達給Object2一些指令。然而由於封裝的緣故,致使Object2無法得知它的父級物件,但這並不妨礙Object2執行這些指令。為了向外傳送訊息,Object2會分發一些事件。如果Object1為自身註冊了針對Object2所發事件的偵聽器,它就能接收到這些事件。Object2與Object3之間的關係也是如此。假如所有這些物件都是顯示物件,那麼被Object3設定為冒泡的事件註定最後會抵達Object1處(前提是Object1註冊了針對這些事件的偵聽器)。你可以將這些物件看成一列同朝著一個方向站立的人群。佇列後面的人能夠看到位於其前面的所有人,並且能直接跟他們講話,即使這番話要直接經過前面的人才能傳遞到聽者處。但是其他人並不知道在他身後站的是誰(如果有人的話),以及他們是否正好在收聽講話。他們能做的只有說話(也就是分發事件),而且他們不在乎所說的話是否能被人聽到。正因為你並不需要知道某個特定物件之上的層級關係,所以為層級加入新的物件就相對容易多了。
如圖12-2所示,我們把Object4加入到了顯示列表的第二層。這時只需改變一點即可,即你要讓Object1知道Object4的資料型別,這樣它才能正確地使用Object4的公共介面。而Object4與Object2的關係也同樣如此。當然,這是一個很抽象而又很簡單的情況,但如果架構考慮得不周詳,作出這樣的改變就會給應用程式的其餘部分帶來災難性後果。 因為各種遊戲在機制與行為上都有很大不同,而且遊戲在測試時其玩法也經常會改變,所以在建立遊戲引擎時必須要保證系統的靈活性。
12.3 單例模式:一種良好的文件模式
儘管我並不贊成用設計模式來進行遊戲開發,但我的確喜歡用一種特殊模式來建立遊戲的文件類。它叫作單例模式(Singleton)。從名稱上你多少就能猜出其含義。採用單例模式構建的類永遠只會在記憶體中存在單獨一個例項,並且為了訪問這個例項,它還提供了全域性訪問指標。用這種模式來建立文件類或者一個站點的頂級類時,它永遠都能保證你能夠輕易地訪問到一些基本的核心功能。比如說,由於遊戲文字需要本地化為另一種語言,所以我們通過一個外部的XML檔案將所有文字載入進來。但我不想在需要時再一遍遍地載入這個XML檔案,所以應該由文件類負責載入它,並使其能被顯示列表中所有物件獲取。單例模式能很好地實現這一點,因為它可以從任何位置建立一個全域性訪問指標,甚至也包括非顯示物件。不過這可是把雙刃劍,濫用該模式會造成儲存過多資料,或者會造成你過多依賴於指向主類的引用,這些都能破壞封裝。在實際應用中,你永遠都不要把對單例類的引用放在你會重用的引擎元件內,因為這樣做會使得引擎變得很僵化。這些引用應該預留給那些要構建具體遊戲的類。接下來就讓我們來看一個單例類。該類檔案位於第12章原始檔夾下,名為SingletonExample.as。
package {
import flash.display.MovieClip;
public class SingletonExample extends MovieClip {
static private var _instance:SingletonExample;
public function SingletonExample(se:SingletonEnforcer) {
if (!se) throw new Error("The SingletonExample class is
a Singleton. Access it via the static getInstance method.");
}
static public function getInstance():SingletonExample {
if (_instance) return _instance;
_instance = new SingletonExample(new SingletonEnforcer());
return _instance;
}
}
}
internal class SingletonEnforcer { }
在傳統上,其他語言會為單例類設定一個private建構函式,以此來防止對該類例項的呼叫。但在AS3中,建構函式只能是public型,所以我們不得不加入一個錯誤檢查來強制使用者正確地使用該類。該類的一個靜態引用指向它唯一的例項,靜態方法getInstance負責將該例項返回。為了防止人們任意將該類例項化,我們還建立了一個只能被主文件類所訪問的私有類。 我們可以將這個類看作Singleton建構函式的金鑰。只有getInstance方法才知道如何正確地建立一個新的SingletonExample類例項,沒有這個金鑰這就無法辦到。這是一種很常用的在AS3中編寫基本單例類的方法,但當我們把這個簡單的例子用作文件類時,它同樣也會失效。這是因為Flash會自動試圖將該類例項化,以便能夠建立顯示列表層級。為了解決這個問題,我們必須修改例項化的時間並改變建構函式的執行方式,同時還要去掉那個私有類。SingletonExample- Document.as就是改變之後的新單例類。
package {
import flash.display.MovieClip;
public class SingletonExampleDocument extends MovieClip {
static private var _instance:SingletonExampleDocument;
public function SingletonExampleDocument() {
if (_instance) throw new Error( " This class is a
Singleton. Access it via the static SingletonExampleDocument.
getInstance method. " );
_instance = this;
addEventListener(Event.REMOVED_FROM_STAGE, onRemove,
false, 0, true);
}
private function onRemove(e:Event):void {
_instance = null;
}
static public function getInstance():SingletonExampleDocument {
if (_instance) return _instance;
_instance = new SingletonExampleDocument();
return _instance;
}
}
}
在這個修改過的版本中,我們利用Flash的特點用建構函式將該類例項化一次。建構函式就會在該例項建立伊始就丟擲錯誤。為了解決該文件有可能被載入另一個SWF這種問題,我們新增了另外一些程式碼。如果遊戲被載入一個容器並且該容器可多次載入並解除安裝遊戲,那麼最好是讓這個單例類例項在其被從舞臺上移除之後能夠實現自我清除。這將防止單例類例項在記憶體中滯留。
你還可以翻回到介紹音訊的第7章去看看另一個實際的單例類範例。SoundEngine類用的就是這種單例模式。單例模式非常適用於建立這些不同種的控制器或者說“引擎”,因為你需要在遊戲的任何位置都能輕鬆地訪問它們。
12.4 本章小結
如果你有心想多學一些遊戲開發所適用的設計模式,那麼可以看看本書網站所連結的一些非常好的文章與書。你最起碼要記住“因地制宜”這四個字,不要過分追求使用那些根本不切合實際的解決方案。沒人會在乎遊戲的實現及設計是否很完美,以及你是否用了“模型—檢視—控制器”設計模式,玩家最終只關心遊戲是否好玩。
相關文章
- Docker 築夢師系列(一):實現容器互聯Docker
- 崢嶸十載,AppLovin不忘初心築夢未來APP
- 期盼已久的“庫許可權”來了
- 在遊戲裡設計房屋的建築師們遊戲
- 【夢想的聲音】
- 作為程式設計師,你的夢想是什麼?程式設計師
- 科學點亮智慧生活航天築夢精彩未來
- 程式設計師如何圓飛行夢想(一)程式設計師
- ARCHICAD 26:塑造未來的建築設計神話
- 阿里雲:開發者是數字文明的建築師阿里
- 【新夢想老師分享】分散式鎖的正確"姿勢"分散式
- 電子採購給建築業帶來的好處
- 關於夢想
- “夢想江湖,從新出發”《新夢想世界》正式開啟
- [AcWing], 蒙德里安的夢想
- 資料標註員:人工智慧行業的“築夢師”丨曼孚科技人工智慧行業
- WebAssembly 的由來Web
- 2020年建築焊工(建築特殊工種)模擬考試題及建築焊工(建築特殊工種)操作證考試
- 建築師解構遊戲關卡——等角檢視的探討遊戲
- 2020年建築電工(建築特殊工種)考試題庫及建築電工(建築特殊工種)考試總結
- 小瓶子大夢想 物理闖關遊戲《瓶子先生和他的夢想》發售遊戲
- 建築轉手稿
- 夢想天空分外藍
- 夢想、理想與妄想
- 你有夢想嗎?華為雲學院助你實現夢想
- 管理建築專案的技巧
- 2020年建築電工(建築特殊工種)考試題庫及建築電工(建築特殊工種)實操考試視訊
- JVM的Eden由來JVM
- 這場期待已久的盛會你要不要來
- 開源!上海AI Lab影片生成大模型書生·築夢 2.0來了AI大模型
- AI工程師的哆啦A夢超能力不是吹出來的!AI工程師
- Vectorworks 2023:創新3D建築設計,打造夢幻之城 mac/win版3DMac
- Vectorworks 2023:引領3D建築創新,塑造夢幻之城 mac/win版3DMac
- 我的夢想是十年內成為架構師,該怎麼辦?架構
- Omdia:“未來兩年4K是市場營銷者的夢想”
- Logic Pro:音樂家的夢想工具
- 夢想是怎樣的顏色?.txt
- ACwing291. 蒙德里安的夢想