鄧 偉
(喀什大學(xué) 計(jì)算機(jī)科學(xué)與技術(shù)學(xué)院,新疆 喀什 844000)
在系統(tǒng)應(yīng)用中,某些對(duì)象要求具有唯一性.例如在Windows 中就只能打開一個(gè)任務(wù)管理器窗口,如果不采用某種機(jī)制對(duì)窗口對(duì)象進(jìn)行唯一化,那么將可能打開多個(gè)窗口,若窗口顯示內(nèi)容一致,則是對(duì)象重復(fù),浪費(fèi)內(nèi)存資源;若窗口顯示內(nèi)容不一致,則意味著在某一瞬間系統(tǒng)有多個(gè)狀態(tài),不知道哪一個(gè)才是真實(shí)的狀態(tài).這樣既不符合實(shí)際,也會(huì)給用戶帶來(lái)混亂.因此,確保系統(tǒng)中某個(gè)對(duì)象的唯一性即一個(gè)類只能有一個(gè)實(shí)例就非常重要[1].這時(shí)常采取單例模式來(lái)保證對(duì)象的唯一性.Unity 3D 是一款專業(yè)的3D 游戲引擎,廣泛用于開發(fā)各種高質(zhì)量2D/3D 游戲和VR/AR 應(yīng)用.游戲開發(fā)中也會(huì)遇到類似的要求,比如游戲設(shè)計(jì)中,可能需要設(shè)計(jì)一個(gè)管理玩家信息的類腳本,要求在整個(gè)游戲中確保該類只有一個(gè)實(shí)例,且便于隨時(shí)訪問(wèn);也可能需要設(shè)計(jì)一個(gè)管理場(chǎng)景切換的類腳本,要求保存之前場(chǎng)景的狀態(tài)或者獲取之前場(chǎng)景的數(shù)據(jù),并能在整個(gè)游戲范圍內(nèi)訪問(wèn)這個(gè)數(shù)據(jù).這時(shí)采用單例模式設(shè)計(jì)類腳本就是一個(gè)比較好的選擇方案,可確保一個(gè)類在系統(tǒng)中只有一個(gè)實(shí)例,并負(fù)責(zé)自行實(shí)例化和向整個(gè)系統(tǒng)提供對(duì)該實(shí)例的訪問(wèn)[2].單例模式在Unity 3D 虛擬現(xiàn)實(shí)和游戲設(shè)計(jì)開發(fā)中,一般常用于管理器類的設(shè)計(jì),實(shí)現(xiàn)場(chǎng)景切換中對(duì)象的唯一性及持久化,是一種重要的設(shè)計(jì)模式.
單例模式(Singleton Pattern) 是一種創(chuàng)建型模式,要求在設(shè)計(jì)一個(gè)類時(shí)保證整個(gè)程序在運(yùn)行期間只存在一個(gè)實(shí)例對(duì)象且易于被訪問(wèn),是對(duì)實(shí)例的生成過(guò)程進(jìn)行管理的一種模式.它提供了全局唯一的訪問(wèn)入口,易于控制可能發(fā)生的沖突,可以嚴(yán)格地控制客戶怎樣訪問(wèn)它以及何時(shí)訪問(wèn)它[3].特殊場(chǎng)合下,如何保證一個(gè)類只有一個(gè)實(shí)例對(duì)象且這個(gè)實(shí)例對(duì)象易于被訪問(wèn),是一個(gè)必須要的考慮因素.定義一個(gè)全局性的變量雖可實(shí)現(xiàn)實(shí)例對(duì)象隨時(shí)被訪問(wèn),卻也可能實(shí)例化出多個(gè)對(duì)象,不能保證實(shí)例的唯一性.最簡(jiǎn)明可靠的方法是,在類的設(shè)計(jì)中,讓類自身保存它的唯一實(shí)例,保證沒有其他實(shí)例被創(chuàng)建,且可提供一個(gè)訪問(wèn)該實(shí)例的方法.單例模式中的這個(gè)類稱為單例類,其有三個(gè)特點(diǎn):?jiǎn)卫愔荒苡幸粋€(gè)實(shí)例;單例類必須自行創(chuàng)建這個(gè)實(shí)例;單例類必須自行向整個(gè)系統(tǒng)提供這個(gè)實(shí)例.可以說(shuō),當(dāng)一個(gè)系統(tǒng)中,只允許某個(gè)類的一個(gè)實(shí)例存在,那么就應(yīng)采用單例模式[4].設(shè)置類為單例,需要考慮三個(gè)問(wèn)題:一是不讓類的實(shí)例被隨意構(gòu)造出來(lái);二是如果不能隨意構(gòu)造類的實(shí)例,那么要使用這個(gè)類該如何解決;三是保證只能構(gòu)造一個(gè)唯一實(shí)例.普通類的實(shí)例是用new 關(guān)鍵字來(lái)創(chuàng)建的,調(diào)用的是類的默認(rèn)公有無(wú)參構(gòu)造器.所以,如果不讓類的實(shí)例被隨意構(gòu)造出來(lái),可以將構(gòu)造器私有化,這樣就不能隨意使用構(gòu)造器來(lái)創(chuàng)建類的實(shí)例.針對(duì)第二個(gè)問(wèn)題,可以在類中調(diào)用自身私有構(gòu)造方法,然后對(duì)外提供一個(gè)公有的靜態(tài)方法來(lái)獲取即可.解決第三個(gè)問(wèn)題,可以聲明一個(gè)私有的靜態(tài)的類的變量,在公有的靜態(tài)方法中,根據(jù)判斷這個(gè)變量的引用是否為null 來(lái)決定是否調(diào)用自身私有構(gòu)造方法創(chuàng)建類的實(shí)例,從而保證實(shí)例的唯一性.
在Unity 3D 中,MonoBehaviour 類是十分重要的類,它定義了基本的腳本行為,提供了許多屬性和方法,不同的方法在特定的情況下會(huì)被調(diào)用,實(shí)現(xiàn)特定的功能.[5]根據(jù)設(shè)計(jì)的類是否繼承自MonoBehaviour 類,可以分為兩類單例模式,一種是非繼承MonoBehaviour 類普通單例模式,另一種是繼承MonoBehaviour 類的單例模式.
設(shè)計(jì)了SingletonClass 類樣例,類圖如圖1所示.
圖1 SingletonClass 類圖
設(shè)計(jì)如下:
代碼中,聲明一個(gè)靜態(tài)SingletonClass 類的變量instance 來(lái)引用唯一的對(duì)象,私有的無(wú)參構(gòu)造方法SingletonClass()讓外部類無(wú)法調(diào)用,只能在單例類的內(nèi)部被調(diào)用,靜態(tài)方法GetSingletonInstance () 內(nèi)通過(guò)判斷instance 是否為null,保證創(chuàng)建此類的唯一對(duì)象.上述代碼可以說(shuō)是一個(gè)純C# 普通單例,在Unity 3D 中,如果自定義一個(gè)類,只是想調(diào)用其中的方法和屬性,不掛載到場(chǎng)景的物體中使用,就可以不繼承MonoBehaviour 類,可以使用以上方式來(lái)構(gòu)建單例,此類方式在Unity 3D 開發(fā)中一般比較少見.
在Unity 3D 中,任何要掛載到游戲?qū)ο笊系哪_本都需要繼承MonoBehaviour 類,因此在Unity 3D 開發(fā)中此類單例模式比較常見.和普通單例不同的是,所有繼承MonoBehaviour 的類不可以實(shí)例化,Unity 3D 都會(huì)自動(dòng)創(chuàng)建實(shí)例,繼承自MonoBeHaviour 的腳本在掛上GameObject的時(shí)候相當(dāng)于已經(jīng)實(shí)例化一次.
設(shè)計(jì)如下:
上述代碼中,單例類SingletonClass 繼承自MonoBehaviour 類,內(nèi)部有一個(gè)instance 屬性,get設(shè)置外部可以讀取,私有的set 保證只能在SingletonClass 類內(nèi)部賦值,Awake()方法在腳本對(duì)象實(shí)例化時(shí)被調(diào)用.
在使用Unity 3D 開發(fā)游戲的時(shí)候,經(jīng)常需要設(shè)計(jì)類用于管理一些全局的變量和方法,記錄各種需要在整個(gè)游戲過(guò)程中持久化的數(shù)據(jù),特別是在場(chǎng)景切換時(shí),要保證對(duì)象的唯一,且可以被訪問(wèn).設(shè)有一個(gè)特定環(huán)境:模仿闖關(guān)游戲中的分值獎(jiǎng)勵(lì).有兩個(gè)游戲場(chǎng)景Scene1 和Scene2,每個(gè)場(chǎng)景中各有一個(gè)按鈕(分別命名為Button1和Button2)和文本UI(命名為Text_Score),點(diǎn)擊一個(gè)場(chǎng)景的按鈕(先啟動(dòng)Scene1 場(chǎng)景),會(huì)跳轉(zhuǎn)到另一個(gè)場(chǎng)景,并且每點(diǎn)擊一次按鈕,跳轉(zhuǎn)到的場(chǎng)景中的文本UI 會(huì)執(zhí)行加1 操作并顯示.
從單例模式角度分析,設(shè)計(jì)一個(gè)Manager類,整個(gè)游戲過(guò)程中必須有且只有一個(gè)Manager類的實(shí)例;在Manager 中記錄一個(gè)名為Score 的int 型屬性變量;場(chǎng)景切換時(shí),Manager 不被銷毀,并對(duì)Score 值進(jìn)行加1 操作;每個(gè)場(chǎng)景中的文本UI 會(huì)顯示當(dāng)前Score 值.
在Unity 3D 中的Scene1 場(chǎng)景下創(chuàng)建一個(gè)空物體對(duì)象,命名為Player.創(chuàng)建C# 腳本,命名為Manager,將Manage 類腳本掛載在Player 對(duì)象上.
上述代碼中,Manager類繼承了MonoBehaviour 類.靜態(tài)Manager 類型的屬性成員Instance 保證它可以通過(guò)類訪問(wèn),而不是通過(guò)實(shí)例訪問(wèn),Instance 屬性中,get 保證外部可讀取,同時(shí)私有的set 保證只能在Manager 類內(nèi)部賦值;Score 屬性實(shí)現(xiàn)對(duì)私有_Score 字段的讀??;Awake()方法在腳本實(shí)例被創(chuàng)建時(shí)調(diào)用,腳本生命周期中只執(zhí)行一次,方法中對(duì)Instance 進(jìn)行賦值初始化.
創(chuàng)建ChangeScene 類腳本,分別掛載到兩個(gè)場(chǎng)景的按鈕上,用于實(shí)現(xiàn)場(chǎng)景切換及計(jì)分顯示功能.
代碼中,Start () 方法中實(shí)現(xiàn)單例對(duì)象中的Score 屬性的讀操作,將分值顯示在文本UI 上.Change()是按鈕事件的處理方法,單擊按鈕時(shí)實(shí)現(xiàn)單例對(duì)象中的Score 屬性進(jìn)行分值加1 的寫操作,分值加1,并跳轉(zhuǎn)到另一場(chǎng)景.
在Unity 3D 中,切換場(chǎng)景時(shí)默認(rèn)會(huì)銷毀上一個(gè)游戲場(chǎng)景中所有的游戲?qū)ο?,所以?chǎng)景中所創(chuàng)建的Player 對(duì)象會(huì)被銷毀,這時(shí)可以使用DontDestroyOnLoad()方法讓Player 對(duì)象在切換游戲場(chǎng)景時(shí)不會(huì)被銷毀,該方法添加在Manager類的Awake () 方法內(nèi)即可.另外,當(dāng)從場(chǎng)景Scene2 切換回場(chǎng)景Scene1 時(shí),并不是恢復(fù)場(chǎng)景Scene1,而是重新創(chuàng)建出一個(gè)新場(chǎng)景,導(dǎo)致一個(gè)新的Player 對(duì)象被創(chuàng)建,不能保證Player 對(duì)象的唯一性.所以需要在Awake()方法內(nèi)添加邏輯判斷,檢測(cè)到已經(jīng)存在一個(gè)Player 對(duì)象時(shí),銷毀當(dāng)前Player 對(duì)象.Awake()方法代碼修改如下:
單例模式是一種常用的設(shè)計(jì)模式,系統(tǒng)中對(duì)應(yīng)類只有一個(gè)實(shí)例對(duì)象,可以避免多次創(chuàng)建對(duì)象帶來(lái)的資源消耗,或是方便數(shù)據(jù)的統(tǒng)一處理和保存.Unity 3D 是單線程操作,通過(guò)協(xié)程機(jī)制來(lái)實(shí)現(xiàn)一些類似于多線程的功能.所以在上述單例模式設(shè)計(jì)中,沒有涉及多線程而去增加“鎖”.此外,如果單例不涉及到Unity 3D 相關(guān)組件操作,就不用繼承MonoBehaviour 類,可以用純C# 的單例語(yǔ)法來(lái)處理.