Unity3D 單例模式和靜態(tài)類的使用詳解
Unity3D的API提供了很多的功能,但是很多流程還是會(huì)自己去封裝一下去。當(dāng)然現(xiàn)在網(wǎng)上也有很多的框架可以去下載使用,但是肯定不會(huì)比自己寫(xiě)的用起來(lái)順手。
對(duì)于是否需要使用框架的問(wèn)題上,本人是持肯定態(tài)度的,把一些常用方法進(jìn)行封裝,做成一個(gè)功能性的框架,可以很大程度上提高代碼的效率,維護(hù)也方便。
對(duì)于網(wǎng)絡(luò)上很多教程上使用的“游戲通用MVC框架”,現(xiàn)在看來(lái)并不符合MVC這種結(jié)構(gòu)性框架的設(shè)計(jì)思想:要知道,MVC最初是被設(shè)計(jì)為Web應(yīng)用的框架,而游戲中的很多事件并不是通過(guò)用戶點(diǎn)擊UI發(fā)生的,View和Controller在游戲邏輯中的占比一般都少的可憐,而且很多教程上把Model剝離出很多“Manager”模塊,甚至有人把View和Controller合在一起寫(xiě)了UIManager——連MVC的結(jié)構(gòu)都沒(méi)了,為啥還要稱之為MVC框架呢?
MVC: “人紅是非多。。。?!?/p>
目前大部分的游戲框架——特別是小型項(xiàng)目的游戲框架——都是把一些數(shù)據(jù)的特定行為進(jìn)行了一下封裝:生成一個(gè)物件,播放一個(gè)特效,進(jìn)行一次隨機(jī)事件等。當(dāng)然也會(huì)有一些結(jié)構(gòu)性的設(shè)計(jì)或者資源管理設(shè)計(jì)如:UI的回退?;蛘呋赝随湥瑘?chǎng)景的載入記錄和切換,下載隊(duì)列的管理等。
在Unity的框架設(shè)計(jì)中,有一個(gè)詞會(huì)經(jīng)常見(jiàn)到:?jiǎn)卫J剑╯ingleton)。單例模式就是在整個(gè)游戲中只使用某個(gè)類的一個(gè)實(shí)例,核心的一句話就是public static T Instance;即在類中定義了一個(gè)靜態(tài)的自身實(shí)例供外部使用,調(diào)用方法時(shí)就是:T.Instance.Function()。在本人最初接觸這種設(shè)計(jì)方式時(shí)經(jīng)常會(huì)與靜態(tài)類弄混淆,T.Function()。中間差了一個(gè)靜態(tài)Instance,很多時(shí)候好像區(qū)別不大。。。
在接近兩周左右的時(shí)間里,我一直在糾結(jié)于自己正在寫(xiě)的框架到底應(yīng)該寫(xiě)成單例模式的還是靜態(tài)模式的,今天剛好對(duì)這個(gè)問(wèn)題有了一個(gè)新的想法:靜態(tài)可不可以理解為一種封閉性很強(qiáng)的單例?
首先回想一下靜態(tài)的兩個(gè)常識(shí):
1、靜態(tài)類不能繼承和被繼承!(嚴(yán)格點(diǎn)說(shuō)是只能繼承System.Object)也就是說(shuō)你的靜態(tài)類不可能去繼承MonoBehaviour,不能實(shí)現(xiàn)接口。
2、靜態(tài)方法不能使用非靜態(tài)成員!如果你大量使用靜態(tài)方法,而方法里又需要用到這個(gè)類的成員,那么你的成員得是靜態(tài)成員。
第2點(diǎn)需要注意:如果你想在Unity的編輯器下調(diào)整某個(gè)參數(shù),那么這個(gè)參數(shù)就不能是靜態(tài)的(哪怕你自定義EditorWindow去修改這個(gè)值也沒(méi)用),解決的辦法是通過(guò)UnityEngine.ScriptableObject去存放配置(生成*.asset文件),然后在運(yùn)行中通過(guò)LoadAsset去加載,然后再改變靜態(tài)成員。至于原因,相信不難理解——你看到的所有Unity組件都是一個(gè)個(gè)實(shí)例,你要通過(guò)Unity的編輯器去配置,那么你就得有一個(gè)這樣的可配置實(shí)例。
從面向?qū)ο笊舷胍幌拢红o態(tài)方法或者靜態(tài)類,不需要依賴對(duì)象,類是唯一的;單例的靜態(tài)實(shí)例,一般就是唯一的一個(gè)對(duì)象(當(dāng)然也可以有多個(gè))。差別嘛。。。好像也不大。。。
如果這樣考慮沒(méi)有錯(cuò),那再回頭比較一下兩種方式:
1、靜態(tài)(靜態(tài)方法或者靜態(tài)類),代碼編寫(xiě)上絆手絆腳,方法調(diào)用很方便,運(yùn)行效率高一丟丟。邏輯面向過(guò)程,不能很好地控制加載和銷毀。
2、單例(類的靜態(tài)實(shí)例),代碼編寫(xiě)和其他類完全一樣,繼承抽象模版接口都可以,Unity里也很方便進(jìn)行參數(shù)配置,不過(guò)使用麻煩有犯錯(cuò)的可能性(必須通過(guò)實(shí)例調(diào)用方法),效率不如靜態(tài)(但是也不會(huì)有很大影響吧)。
如果這些說(shuō)法太抽象,那我再給出一個(gè)常見(jiàn)的問(wèn)題:如果你的框架有一個(gè)SoundManager能夠管理所有的聲音播放,那么你會(huì)怎么去實(shí)現(xiàn)?
(在剛接觸AudioSource這個(gè)組件的時(shí)候,我想的是每一個(gè)聲音都由一個(gè)AudioSource去播放。但是后來(lái)發(fā)現(xiàn)完全沒(méi)必要,AudioSource有靜態(tài)的PlayClipAtPoint方法去播放臨時(shí)3D音效,同時(shí)有實(shí)例方法PlayOneShot去播放臨時(shí)音效(2D和3D取決于當(dāng)實(shí)例的SpatialBlend)。如果沒(méi)有特殊的需求,那么一個(gè)AudioSource循環(huán)播放背景音樂(lè),上述兩種方法播放游戲中的特效音頻,這對(duì)于大部分游戲已經(jīng)足夠了。)
那么問(wèn)題來(lái)了:你的SoundManager播放聲音的方法如果是靜態(tài)的,那么AudioSource組件必須在代碼中通過(guò)各種方式去獲?。ㄐ陆ńM件或者獲取特定GameObject下的組件)——因?yàn)楸4孢@個(gè)組件的變量必須是靜態(tài)的,也就不能通過(guò)Unity的編輯器去賦值。如果不去閱讀代碼那么用戶完全不知道這是一個(gè)什么樣的組件獲取流程,如果我破壞這個(gè)流程(同名物體,包含互斥組件等),那么這個(gè)Manager很有可能會(huì)出現(xiàn)不可預(yù)料的異常。
而繼承MonoBehaviour并RequireComponent(typeof(AudioSource)),怎么看也比“為了靜態(tài)而靜態(tài)”的代碼要方便健壯的多。
實(shí)際上到這里已經(jīng)可以基本總結(jié)出何時(shí)需要使用單例了:
1、只要你的類需要保存其他組件作為變量,那么就有必要使用單例;
2、只要你有在Unity編輯器上進(jìn)行參數(shù)配置的需求,那么就有必要使用單例;
3、只要你的管理器需要進(jìn)行加載的順序控制,那么就有必要使用單例(比如熱更新之后加載ResourcesManager);
當(dāng)然,這里都只是“有必要”,并不是“必須”。兩者區(qū)別最大的地方,一個(gè)是方便寫(xiě),一個(gè)是方便用。方便寫(xiě)的代價(jià)是每次調(diào)用加個(gè)instance,方便用的代價(jià)則是放棄了面向?qū)ο蠛蚒nity的“所見(jiàn)即所得”,孰輕孰重,自己抉擇。
另一方面,和“為了靜態(tài)而靜態(tài)”一樣,“為了單例而單例”同樣是一個(gè)不合理的設(shè)計(jì)。這樣的解釋仍然是那么的模糊,那么,就給自己定義一個(gè)最簡(jiǎn)單的規(guī)則吧——如果你的單例類里沒(méi)有任何需要保存狀態(tài)的變量,那么這個(gè)類里的方法就可以全都是靜態(tài)方法,這個(gè)類也可以是個(gè)靜態(tài)類。
補(bǔ)充:從實(shí)例出發(fā),了解單例模式和靜態(tài)塊
就算你沒(méi)有用到過(guò)其他的設(shè)計(jì)模式,但是單例模式你肯定接觸過(guò),比如,Spring 中 bean 默認(rèn)就是單例模式的,所有用到這個(gè) bean 的實(shí)例其實(shí)都是同一個(gè)。
單例模式的使用場(chǎng)景
什么是單例模式呢,單例模式(Singleton)又叫單態(tài)模式,它出現(xiàn)目的是為了保證一個(gè)類在系統(tǒng)中只有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。從這點(diǎn)可以看出,單例模式的出現(xiàn)是為了可以保證系統(tǒng)中一個(gè)類只有一個(gè)實(shí)例而且該實(shí)例又易于外界訪問(wèn),從而方便對(duì)實(shí)例個(gè)數(shù)的控制并節(jié)約系統(tǒng)資源而出現(xiàn)的解決方案。
使用單例模式當(dāng)然是有原因,有好處的了。在下面幾個(gè)場(chǎng)景中適合使用單例模式:
1、有頻繁實(shí)例化然后銷毀的情況,也就是頻繁的 new 對(duì)象,可以考慮單例模式;
2、創(chuàng)建對(duì)象時(shí)耗時(shí)過(guò)多或者耗資源過(guò)多,但又經(jīng)常用到的對(duì)象;
3、頻繁訪問(wèn) IO 資源的對(duì)象,例如數(shù)據(jù)庫(kù)連接池或訪問(wèn)本地文件;
下面舉幾個(gè)例子來(lái)說(shuō)明一下:
1、網(wǎng)站在線人數(shù)統(tǒng)計(jì);
其實(shí)就是全局計(jì)數(shù)器,也就是說(shuō)所有用戶在相同的時(shí)刻獲取到的在線人數(shù)數(shù)量都是一致的。要實(shí)現(xiàn)這個(gè)需求,計(jì)數(shù)器就要全局唯一,也就正好可以用單例模式來(lái)實(shí)現(xiàn)。當(dāng)然這里不包括分布式場(chǎng)景,因?yàn)橛?jì)數(shù)是存在內(nèi)存中的,并且還要保證線程安全。下面代碼是一個(gè)簡(jiǎn)單的計(jì)數(shù)器實(shí)現(xiàn)。
public class Counter { private static class CounterHolder{ private static final Counter counter = new Counter(); } private Counter(){ System.out.println("init..."); } public static final Counter getInstance(){ return CounterHolder.counter; } private AtomicLong online = new AtomicLong(); public long getOnline(){ return online.get(); } public long add(){ return online.incrementAndGet(); } }
2、配置文件訪問(wèn)類;
項(xiàng)目中經(jīng)常需要一些環(huán)境相關(guān)的配置文件,比如短信通知相關(guān)的、郵件相關(guān)的。比如 properties 文件,這里就以讀取一個(gè)properties 文件配置為例,如果你使用的 Spring ,可以用 @PropertySource 注解實(shí)現(xiàn),默認(rèn)就是單例模式。如果不用單例的話,每次都要 new 對(duì)象,每次都要重新讀一遍配置文件,很影響性能,如果用單例模式,則只需要讀取一遍就好了。以下是文件訪問(wèn)單例類簡(jiǎn)單實(shí)現(xiàn):
public class SingleProperty { private static Properties prop; private static class SinglePropertyHolder{ private static final SingleProperty singleProperty = new SingleProperty(); } /** * config.properties 內(nèi)容是 test.name=kite */ private SingleProperty(){ System.out.println("構(gòu)造函數(shù)執(zhí)行"); prop = new Properties(); InputStream stream = SingleProperty.class.getClassLoader() .getResourceAsStream("config.properties"); try { prop.load(new InputStreamReader(stream, "utf-8")); } catch (IOException e) { e.printStackTrace(); } } public static SingleProperty getInstance(){ return SinglePropertyHolder.singleProperty; } public String getName(){ return prop.get("test.name").toString(); } public static void main(String[] args){ SingleProperty singleProperty = SingleProperty.getInstance(); System.out.println(singleProperty.getName()); } }
3、數(shù)據(jù)庫(kù)連接池的實(shí)現(xiàn),也包括線程池。
為什么要做池化,是因?yàn)樾陆ㄟB接很耗時(shí),如果每次新任務(wù)來(lái)了,都新建連接,那對(duì)性能的影響實(shí)在太大。所以一般的做法是在一個(gè)應(yīng)用內(nèi)維護(hù)一個(gè)連接池,這樣當(dāng)任務(wù)進(jìn)來(lái)時(shí),如果有空閑連接,可以直接拿來(lái)用,省去了初始化的開(kāi)銷。
所以用單例模式,正好可以實(shí)現(xiàn)一個(gè)應(yīng)用內(nèi)只有一個(gè)線程池的存在,所有需要連接的任務(wù),都要從這個(gè)連接池來(lái)獲取連接。
如果不使用單例,那么應(yīng)用內(nèi)就會(huì)出現(xiàn)多個(gè)連接池,那也就沒(méi)什么意義了。如果你使用 Spring 的話,并集成了例如 druid 或者 c3p0 ,這些成熟開(kāi)源的數(shù)據(jù)庫(kù)連接池,一般也都是默認(rèn)以單例模式實(shí)現(xiàn)的。
單例模式的實(shí)現(xiàn)方法
如果你在書(shū)上或者網(wǎng)站上搜索單例模式的實(shí)現(xiàn),一般都會(huì)介紹5、6中方式,其中有一些隨著 Java 版本的升高,以及多線程技術(shù)的使用變得不那么實(shí)用了,這里就介紹兩種即高效,而且又是線程安全的方式。
1. 靜態(tài)內(nèi)部類方式
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
這種寫(xiě)法仍然使用 JVM 本身機(jī)制保證了線程安全問(wèn)題,由于 SingletonHolder 是私有的,除了 getInstance() 方法外沒(méi)有辦法訪問(wèn)它,因此它是懶漢式的;同時(shí)讀取實(shí)例的時(shí)候不會(huì)進(jìn)行同步,沒(méi)有性能缺陷;也不依賴 JDK 版本。上面的兩個(gè)例子就是用這種方式實(shí)現(xiàn)的。
2. 枚舉方式
public enum SingleEnum { INSTANCE; SingleEnum(){ System.out.println("構(gòu)造函數(shù)執(zhí)行"); } public String getName(){ return "singleEnum"; } public static void main(String[] args){ SingleEnum singleEnum = SingleEnum.INSTANCE; System.out.println(singleEnum.getName()); } }
我們可以通過(guò) SingleEnum.INSTANCE 來(lái)訪問(wèn)實(shí)例。而且創(chuàng)建枚舉默認(rèn)就是線程安全的,并且還能防止反序列化導(dǎo)致重新創(chuàng)建新的對(duì)象。
靜態(tài)塊
什么是靜態(tài)塊呢
1、它是隨著類的加載而執(zhí)行,只執(zhí)行一次,并優(yōu)先于主函數(shù)。具體說(shuō),靜態(tài)代碼塊是由類調(diào)用的。類調(diào)用時(shí),先執(zhí)行靜態(tài)代碼塊,然后才執(zhí)行主函數(shù)的;
2、靜態(tài)代碼塊其實(shí)就是給類初始化的,而構(gòu)造代碼塊是給對(duì)象初始化的;
3、靜態(tài)代碼塊中的變量是局部變量,與普通函數(shù)中的局部變量性質(zhì)沒(méi)有區(qū)別;
4、一個(gè)類中可以有多個(gè)靜態(tài)代碼塊;
他的寫(xiě)法是這樣的:
static { System.out.println("static executed"); }
來(lái)看一下下面這個(gè)完整的實(shí)例:
public class SingleStatic { static { System.out.println("static 塊執(zhí)行中..."); } { System.out.println("構(gòu)造代碼塊 執(zhí)行中..."); } public SingleStatic(){ System.out.println("構(gòu)造函數(shù) 執(zhí)行中"); } public static void main(String[] args){ System.out.println("main 函數(shù)執(zhí)行中"); SingleStatic singleStatic = new SingleStatic(); } }
他的執(zhí)行結(jié)果是這樣的:
static 塊執(zhí)行中...
main 函數(shù)執(zhí)行中
構(gòu)造代碼塊 執(zhí)行中...
構(gòu)造函數(shù) 執(zhí)行中
從中可以看出他們的執(zhí)行順序分別為:
1、靜態(tài)代碼塊
2、main 函數(shù)
3、構(gòu)造代碼塊
4、構(gòu)造函數(shù)
利用靜態(tài)代碼塊只在類加載的時(shí)候執(zhí)行,并且只執(zhí)行一次這個(gè)特性,也可以用來(lái)實(shí)現(xiàn)單例模式,但是不是懶加載,也就是說(shuō)每次類加載就會(huì)主動(dòng)觸發(fā)實(shí)例化。
除此之外,不考慮單例的情況,利用靜態(tài)代碼塊的這個(gè)特性,可以實(shí)現(xiàn)其他的一些功能,例如上面提到的配置文件加載的功能,可以在類加載的時(shí)候就讀取配置文件的內(nèi)容,相當(dāng)于一個(gè)預(yù)加載的功能,在使用的時(shí)候可以直接拿來(lái)就用。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
- 在unity腳本中控制Inspector面板的參數(shù)操作
- C#中public變量不能被unity面板識(shí)別的解決方案
- Unity使用物理引擎實(shí)現(xiàn)多旋翼無(wú)人機(jī)的模擬飛行
- 在Unity中使用全局變量的操作
- unity 切換場(chǎng)景不銷毀物體問(wèn)題的解決
- Unity 靜態(tài)變量跨場(chǎng)景操作
- Unity 讀取文件 TextAsset讀取配置文件方式
- 解決在Unity中使用FairyGUI遇到的坑
- Unity3d 如何更改Button的背景色
- Unity3d使用FairyGUI 自定義字體的操作
- Unity3D運(yùn)行報(bào)DllNotFoundException錯(cuò)誤的解決方案
- Unity游戲之存儲(chǔ)數(shù)據(jù)
相關(guān)文章
C#基于WebSocket實(shí)現(xiàn)聊天室功能
這篇文章主要為大家詳細(xì)介紹了C#基于WebSocket實(shí)現(xiàn)聊天室功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02WPF利用LiveCharts實(shí)現(xiàn)動(dòng)態(tài)曲線圖繪制
LiveCharts是一個(gè)比較漂亮的WPF圖表控件,在數(shù)據(jù)發(fā)生變化后,還可以設(shè)置相對(duì)于的動(dòng)畫(huà)效果,本文就來(lái)利用LiveCharts繪制簡(jiǎn)單的動(dòng)態(tài)曲線圖吧2023-10-10Unity shader實(shí)現(xiàn)頂點(diǎn)動(dòng)畫(huà)波動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Unity shader實(shí)現(xiàn)頂點(diǎn)動(dòng)畫(huà)波動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04Unity游戲開(kāi)發(fā)實(shí)現(xiàn)場(chǎng)景切換示例
這篇文章主要為大家介紹了Unity游戲開(kāi)發(fā)實(shí)現(xiàn)場(chǎng)景切換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08