Unity3D 單例模式和靜態(tài)類的使用詳解
Unity3D的API提供了很多的功能,但是很多流程還是會自己去封裝一下去。當然現(xiàn)在網(wǎng)上也有很多的框架可以去下載使用,但是肯定不會比自己寫的用起來順手。
對于是否需要使用框架的問題上,本人是持肯定態(tài)度的,把一些常用方法進行封裝,做成一個功能性的框架,可以很大程度上提高代碼的效率,維護也方便。
對于網(wǎng)絡上很多教程上使用的“游戲通用MVC框架”,現(xiàn)在看來并不符合MVC這種結(jié)構(gòu)性框架的設計思想:要知道,MVC最初是被設計為Web應用的框架,而游戲中的很多事件并不是通過用戶點擊UI發(fā)生的,View和Controller在游戲邏輯中的占比一般都少的可憐,而且很多教程上把Model剝離出很多“Manager”模塊,甚至有人把View和Controller合在一起寫了UIManager——連MVC的結(jié)構(gòu)都沒了,為啥還要稱之為MVC框架呢?
MVC: “人紅是非多。。。?!?/p>
目前大部分的游戲框架——特別是小型項目的游戲框架——都是把一些數(shù)據(jù)的特定行為進行了一下封裝:生成一個物件,播放一個特效,進行一次隨機事件等。當然也會有一些結(jié)構(gòu)性的設計或者資源管理設計如:UI的回退?;蛘呋赝随?,場景的載入記錄和切換,下載隊列的管理等。
在Unity的框架設計中,有一個詞會經(jīng)常見到:單例模式(singleton)。單例模式就是在整個游戲中只使用某個類的一個實例,核心的一句話就是public static T Instance;即在類中定義了一個靜態(tài)的自身實例供外部使用,調(diào)用方法時就是:T.Instance.Function()。在本人最初接觸這種設計方式時經(jīng)常會與靜態(tài)類弄混淆,T.Function()。中間差了一個靜態(tài)Instance,很多時候好像區(qū)別不大。。。
在接近兩周左右的時間里,我一直在糾結(jié)于自己正在寫的框架到底應該寫成單例模式的還是靜態(tài)模式的,今天剛好對這個問題有了一個新的想法:靜態(tài)可不可以理解為一種封閉性很強的單例?
首先回想一下靜態(tài)的兩個常識:
1、靜態(tài)類不能繼承和被繼承!(嚴格點說是只能繼承System.Object)也就是說你的靜態(tài)類不可能去繼承MonoBehaviour,不能實現(xiàn)接口。
2、靜態(tài)方法不能使用非靜態(tài)成員!如果你大量使用靜態(tài)方法,而方法里又需要用到這個類的成員,那么你的成員得是靜態(tài)成員。
第2點需要注意:如果你想在Unity的編輯器下調(diào)整某個參數(shù),那么這個參數(shù)就不能是靜態(tài)的(哪怕你自定義EditorWindow去修改這個值也沒用),解決的辦法是通過UnityEngine.ScriptableObject去存放配置(生成*.asset文件),然后在運行中通過LoadAsset去加載,然后再改變靜態(tài)成員。至于原因,相信不難理解——你看到的所有Unity組件都是一個個實例,你要通過Unity的編輯器去配置,那么你就得有一個這樣的可配置實例。
從面向?qū)ο笊舷胍幌拢红o態(tài)方法或者靜態(tài)類,不需要依賴對象,類是唯一的;單例的靜態(tài)實例,一般就是唯一的一個對象(當然也可以有多個)。差別嘛。。。好像也不大。。。
如果這樣考慮沒有錯,那再回頭比較一下兩種方式:
1、靜態(tài)(靜態(tài)方法或者靜態(tài)類),代碼編寫上絆手絆腳,方法調(diào)用很方便,運行效率高一丟丟。邏輯面向過程,不能很好地控制加載和銷毀。
2、單例(類的靜態(tài)實例),代碼編寫和其他類完全一樣,繼承抽象模版接口都可以,Unity里也很方便進行參數(shù)配置,不過使用麻煩有犯錯的可能性(必須通過實例調(diào)用方法),效率不如靜態(tài)(但是也不會有很大影響吧)。
如果這些說法太抽象,那我再給出一個常見的問題:如果你的框架有一個SoundManager能夠管理所有的聲音播放,那么你會怎么去實現(xiàn)?
(在剛接觸AudioSource這個組件的時候,我想的是每一個聲音都由一個AudioSource去播放。但是后來發(fā)現(xiàn)完全沒必要,AudioSource有靜態(tài)的PlayClipAtPoint方法去播放臨時3D音效,同時有實例方法PlayOneShot去播放臨時音效(2D和3D取決于當實例的SpatialBlend)。如果沒有特殊的需求,那么一個AudioSource循環(huán)播放背景音樂,上述兩種方法播放游戲中的特效音頻,這對于大部分游戲已經(jīng)足夠了。)
那么問題來了:你的SoundManager播放聲音的方法如果是靜態(tài)的,那么AudioSource組件必須在代碼中通過各種方式去獲?。ㄐ陆ńM件或者獲取特定GameObject下的組件)——因為保存這個組件的變量必須是靜態(tài)的,也就不能通過Unity的編輯器去賦值。如果不去閱讀代碼那么用戶完全不知道這是一個什么樣的組件獲取流程,如果我破壞這個流程(同名物體,包含互斥組件等),那么這個Manager很有可能會出現(xiàn)不可預料的異常。
而繼承MonoBehaviour并RequireComponent(typeof(AudioSource)),怎么看也比“為了靜態(tài)而靜態(tài)”的代碼要方便健壯的多。
實際上到這里已經(jīng)可以基本總結(jié)出何時需要使用單例了:
1、只要你的類需要保存其他組件作為變量,那么就有必要使用單例;
2、只要你有在Unity編輯器上進行參數(shù)配置的需求,那么就有必要使用單例;
3、只要你的管理器需要進行加載的順序控制,那么就有必要使用單例(比如熱更新之后加載ResourcesManager);
當然,這里都只是“有必要”,并不是“必須”。兩者區(qū)別最大的地方,一個是方便寫,一個是方便用。方便寫的代價是每次調(diào)用加個instance,方便用的代價則是放棄了面向?qū)ο蠛蚒nity的“所見即所得”,孰輕孰重,自己抉擇。
另一方面,和“為了靜態(tài)而靜態(tài)”一樣,“為了單例而單例”同樣是一個不合理的設計。這樣的解釋仍然是那么的模糊,那么,就給自己定義一個最簡單的規(guī)則吧——如果你的單例類里沒有任何需要保存狀態(tài)的變量,那么這個類里的方法就可以全都是靜態(tài)方法,這個類也可以是個靜態(tài)類。
補充:從實例出發(fā),了解單例模式和靜態(tài)塊
就算你沒有用到過其他的設計模式,但是單例模式你肯定接觸過,比如,Spring 中 bean 默認就是單例模式的,所有用到這個 bean 的實例其實都是同一個。
單例模式的使用場景
什么是單例模式呢,單例模式(Singleton)又叫單態(tài)模式,它出現(xiàn)目的是為了保證一個類在系統(tǒng)中只有一個實例,并提供一個訪問它的全局訪問點。從這點可以看出,單例模式的出現(xiàn)是為了可以保證系統(tǒng)中一個類只有一個實例而且該實例又易于外界訪問,從而方便對實例個數(shù)的控制并節(jié)約系統(tǒng)資源而出現(xiàn)的解決方案。
使用單例模式當然是有原因,有好處的了。在下面幾個場景中適合使用單例模式:
1、有頻繁實例化然后銷毀的情況,也就是頻繁的 new 對象,可以考慮單例模式;
2、創(chuàng)建對象時耗時過多或者耗資源過多,但又經(jīng)常用到的對象;
3、頻繁訪問 IO 資源的對象,例如數(shù)據(jù)庫連接池或訪問本地文件;
下面舉幾個例子來說明一下:
1、網(wǎng)站在線人數(shù)統(tǒng)計;
其實就是全局計數(shù)器,也就是說所有用戶在相同的時刻獲取到的在線人數(shù)數(shù)量都是一致的。要實現(xiàn)這個需求,計數(shù)器就要全局唯一,也就正好可以用單例模式來實現(xiàn)。當然這里不包括分布式場景,因為計數(shù)是存在內(nèi)存中的,并且還要保證線程安全。下面代碼是一個簡單的計數(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、配置文件訪問類;
項目中經(jīng)常需要一些環(huán)境相關(guān)的配置文件,比如短信通知相關(guān)的、郵件相關(guān)的。比如 properties 文件,這里就以讀取一個properties 文件配置為例,如果你使用的 Spring ,可以用 @PropertySource 注解實現(xiàn),默認就是單例模式。如果不用單例的話,每次都要 new 對象,每次都要重新讀一遍配置文件,很影響性能,如果用單例模式,則只需要讀取一遍就好了。以下是文件訪問單例類簡單實現(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ù)庫連接池的實現(xiàn),也包括線程池。
為什么要做池化,是因為新建連接很耗時,如果每次新任務來了,都新建連接,那對性能的影響實在太大。所以一般的做法是在一個應用內(nèi)維護一個連接池,這樣當任務進來時,如果有空閑連接,可以直接拿來用,省去了初始化的開銷。
所以用單例模式,正好可以實現(xiàn)一個應用內(nèi)只有一個線程池的存在,所有需要連接的任務,都要從這個連接池來獲取連接。
如果不使用單例,那么應用內(nèi)就會出現(xiàn)多個連接池,那也就沒什么意義了。如果你使用 Spring 的話,并集成了例如 druid 或者 c3p0 ,這些成熟開源的數(shù)據(jù)庫連接池,一般也都是默認以單例模式實現(xiàn)的。
單例模式的實現(xiàn)方法
如果你在書上或者網(wǎng)站上搜索單例模式的實現(xiàn),一般都會介紹5、6中方式,其中有一些隨著 Java 版本的升高,以及多線程技術(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;
}
}
這種寫法仍然使用 JVM 本身機制保證了線程安全問題,由于 SingletonHolder 是私有的,除了 getInstance() 方法外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。上面的兩個例子就是用這種方式實現(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());
}
}
我們可以通過 SingleEnum.INSTANCE 來訪問實例。而且創(chuàng)建枚舉默認就是線程安全的,并且還能防止反序列化導致重新創(chuàng)建新的對象。
靜態(tài)塊
什么是靜態(tài)塊呢
1、它是隨著類的加載而執(zhí)行,只執(zhí)行一次,并優(yōu)先于主函數(shù)。具體說,靜態(tài)代碼塊是由類調(diào)用的。類調(diào)用時,先執(zhí)行靜態(tài)代碼塊,然后才執(zhí)行主函數(shù)的;
2、靜態(tài)代碼塊其實就是給類初始化的,而構(gòu)造代碼塊是給對象初始化的;
3、靜態(tài)代碼塊中的變量是局部變量,與普通函數(shù)中的局部變量性質(zhì)沒有區(qū)別;
4、一個類中可以有多個靜態(tài)代碼塊;
他的寫法是這樣的:
static {
System.out.println("static executed");
}
來看一下下面這個完整的實例:
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)代碼塊只在類加載的時候執(zhí)行,并且只執(zhí)行一次這個特性,也可以用來實現(xiàn)單例模式,但是不是懶加載,也就是說每次類加載就會主動觸發(fā)實例化。
除此之外,不考慮單例的情況,利用靜態(tài)代碼塊的這個特性,可以實現(xiàn)其他的一些功能,例如上面提到的配置文件加載的功能,可以在類加載的時候就讀取配置文件的內(nèi)容,相當于一個預加載的功能,在使用的時候可以直接拿來就用。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
- 在unity腳本中控制Inspector面板的參數(shù)操作
- C#中public變量不能被unity面板識別的解決方案
- Unity使用物理引擎實現(xiàn)多旋翼無人機的模擬飛行
- 在Unity中使用全局變量的操作
- unity 切換場景不銷毀物體問題的解決
- Unity 靜態(tài)變量跨場景操作
- Unity 讀取文件 TextAsset讀取配置文件方式
- 解決在Unity中使用FairyGUI遇到的坑
- Unity3d 如何更改Button的背景色
- Unity3d使用FairyGUI 自定義字體的操作
- Unity3D運行報DllNotFoundException錯誤的解決方案
- Unity游戲之存儲數(shù)據(jù)
相關(guān)文章
基于使用遞歸推算指定位數(shù)的斐波那契數(shù)列值的解決方法
本篇文章介紹了,基于使用遞歸推算指定位數(shù)的斐波那契數(shù)列值的解決方法。需要的朋友參考下2013-05-05
Visual Studio中根據(jù)系統(tǒng)區(qū)分引用64位、32位DLL動態(tài)庫文件的配置方法
這篇文章主要介紹了Visual Studio中根據(jù)系統(tǒng)區(qū)分引用64位、32位DLL動態(tài)庫文件的配置方法,本文在VS2008中測試通過,其它VS版本可以參考下2014-09-09
C#使用開源驅(qū)動連接操作MySQL數(shù)據(jù)庫
這篇文章主要介紹了C#使用開源驅(qū)動連接操作MySQL數(shù)據(jù)庫,本文講解使用SourceForge上的mysqldrivercs驅(qū)動連接操作MySQL數(shù)據(jù)庫,需要的朋友可以參考下2015-02-02
AjaxControlToolkit AjaxFileUpload 顯示英文改成中文的解決方法
AjaxControlToolkit AjaxFileUpload 顯示英文改成中文的解決方法,需要的朋友可以參考一下2013-03-03
C#使用RegNotifyChangeKeyValue監(jiān)聽注冊表更改的方法小結(jié)
RegNotifyChangeKeyValue的最后一個參數(shù)傳遞false,表示以同步的方式監(jiān)聽,這篇文章主要介紹了C#使用RegNotifyChangeKeyValue監(jiān)聽注冊表更改的方法小結(jié),需要的朋友可以參考下2024-06-06

