此篇總結自siki學院的<<暗黑戰神>>課程
上一篇文章介紹過一種常用的游戲開發架構:https://www.cnblogs.com/czw52460183/p/11010971.html,現在我們基於這種架構記錄一種優化小技巧。
先來看下上一篇文章的重點:
一種常見的游戲開發架構思路是創建一個空物體,將一個總管理模塊的腳本掛在此物體下,它負責啟動游戲並初始化各個模塊,根據游戲中各個部分功能的不同,不同的腳本大致可分為公共服務模塊和單個業務系統模塊,公共服務模塊會向系統中的所有模塊提供一些公共服務,比如資源加載,音頻播放,網絡服務等等。
單個業務系統模塊分為很多種,比如登錄系統業務模塊,戰斗業務模塊,副本業務模塊等等,各個單獨業務模塊負責管理本業務相關的腳本,比如某些UI窗口下是某個業務模塊的界面,每個UI窗口下都會有對應的窗口管理腳本,這些窗口管理腳本就是由對應歸屬的單個業務系統模塊腳本來操控的,反映到代碼上,就是這些業務模塊腳本會持有這些窗口管理腳本的引用。
而公共服務模塊和單個業務系統模塊,是統一由管理模塊初始化的,因此管理模塊中需要持有對這兩個模塊的引用,我們可以不使用拖拽的方式來實現,可以將這兩個模塊與管理模塊一起,放在空物體下,管理模塊初始化時用代碼獲取這兩個模塊的引用,並控制這兩個模塊的初始化順序。
注意,比較特殊的是某些窗口管理腳本並不歸屬於任何一個業務模塊,比如加載界面管理,又如動態窗口展示界面管理,這兩種界面是每個業務模塊都有可能用到的,它其實類似於公共服務模塊,但由於它們與某個UI界面關聯,因此可以直接由總管理模塊來控制。
現在假設有這么一種情況:假設我們要做一個加載登錄場景,效果是:進入游戲時,顯示一個加載的界面,界面上有進度條,此時會異步加載另一個場景,進度條隨着加載而移動,場景加載完畢時,加載界面消失,同時在已加載的場景中顯示注冊登錄界面。
如何實現上述效果?
基本思路如下:這里牽涉3個模塊的交互,總管理模塊負責初始化公共服務模塊(這里用到的是資源加載服務模塊)和單個業務系統模塊(這里是登錄注冊業務模塊)。隨后從登錄注冊業務模塊提供的方法來實現加載場景,加載的時候分三步:一,展示加載界面。二,異步加載場景並顯示進度。三,取消展示加載界面並展示登錄注冊界面。
之前說了,資源加載服務是公共服務模塊,因此在登錄注冊業務模塊中可以以單例形式去調用並使用這個服務,那么現在問題來了:
顯示加載界面的代碼放哪里?
由於牽涉到模塊的交互,因此顯示加載界面的代碼即可以放在登錄注冊業務模塊中,也可以放在資源加載服務模塊中,但由於其他業務模塊也會用到加載服務,也要顯示加載界面,因此,最好把顯示加載界面的代碼放在資源加載服務模塊中。
注意,要顯示加載界面,就要取到加載界面對應窗口腳本的引用,由於加載界面直接由總管理模塊控制,因此不要在資源加載服務模塊中持有此腳本的引用,而是要通過總管理模塊間接訪問加載界面,這樣層次更清晰。
異步加載場景在資源加載服務模塊中完成,怎么實時更新進度條呢?
基本思路是調用資源加載函數后,用Update,在每一幀觀測加載的進度(加載函數的調用有個返回值,里面存着加載進度屬性)並進行UI的更新。注意,這里很重要的一點是,我們怎么在Update里獲取到加載函數調用的返回值,難道是以參數形式把加載函數調用的返回值傳遞給Update嗎?不是的,這就體現出委托的重要性了,我們可以在加載函數調用時設置一個委托方法,在委托方法里去查看這個返回值的加載進度,並進行UI更新,而Update里只進行委托的調用。
發現沒,委托最神奇的一點就是可以幫助我們在一個函數中,以不傳值的形式調用另一函數,而且可以用到另一函數中的臨時變量!!
其實這里面的機制我到現在也不大明白,似乎類似於協程?不管怎么說,就像我在https://www.cnblogs.com/czw52460183/p/10494285.html中說的一樣,這種機制為程序設計提供了很大的便利,但似乎也有着破環封裝性的危險,畢竟另一函數,即使是某個類的私有方法,一樣能通過委托,在另一個類中被調用。
更新完進度條后還要檢測進度是否完成,若完成,顯然要清空委托,防止在Update中繼續被調用,同時要取消展示加載界面,並展示登錄界面,這里又有一個問題:登錄界面怎么展示?難道在資源加載服務模塊中持有登錄界面窗口管理腳本的引用嗎?
當然不是,登錄界面窗口是屬於登錄注冊業務模塊的,因此肯定要通過這個模塊去訪問此界面的腳本引用。
那是否可以把登錄注冊業務模塊設置成單例的,在資源加載服務模塊中去調用它,隨后去進行登錄界面展示呢?
也不是,因為這樣的話,其他業務系統中要使用資源加載服務,到最后也會展示登錄注冊窗口界面,顯然不行。怎么辦?
答案也是使用委托,由於每個業務系統在使用加載服務完成后要顯示的界面不同,因此可以在業務模塊調用資源加載服務時,傳入一個委托,該委托負責打開業務模塊自己持有管理的一些界面,相應地,資源加載服務模塊只要在加載完成后,調用此委托即可。
以上就是游戲開發的基本架構,舉了實現異步加載登錄場景的例子進行說明,相關代碼如下:
/**************************************************** 文件:GameRoot.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/12 22:40:42 功能:游戲啟動入口 *****************************************************/ using UnityEngine; public class GameRoot : MonoBehaviour { //GameRoot可以被各系統用來訪問公共界面,所以設置成單例 public static GameRoot Instance = null; //加載進度界面和動態元素界面是公用的,由GameRoot持有 public LoadingWnd loadingWnd; private void Start() { Instance = this; //切換場景時為了防止本場景物體被銷毀,手動指定GameRoot不銷毀,且把所有UI元素掛在其下保護起來 DontDestroyOnLoad(this); Debug.Log("游戲啟動..."); Init(); } //初始化各個模塊 private void Init() { //初始化資源加載服務模塊 ResSvc res = GetComponent<ResSvc>(); res.InitSvc(); //初始化登錄注冊業務系統模塊 LoginSys login = GetComponent<LoginSys>(); login.InitSys(); //進入登錄場景並加載相應UI login.EnterLogin(); } }
為了方便說明,我們只展示代碼中的和架構有關的部分:
上面的是總管理腳本,它直接管理公共的加載進度界面,各個模塊初始化完成后(注意初始化順序),通過登錄注冊業務模塊提供的方法,開始加載場景。
/**************************************************** 文件:LoginSys.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/12 22:47:56 功能:登錄注冊業務模塊 *****************************************************/ using UnityEngine; public class LoginSys : MonoBehaviour { //單例 public static LoginSys Instance = null; //登錄注冊業務模塊下有登錄注冊界面 public LoginWnd loginWnd; //初始化模塊 public void InitSys() { Instance = this; Debug.Log("登錄注冊業務模塊加載完畢..."); } /// <summary> /// 進入登錄場景 /// </summary> public void EnterLogin() { //加載登錄場景 ResSvc.Instance.AsyncLoadScene(Constants.SceneLogin,()=>{ //加載完成后打開登錄注冊界面 loginWnd.gameObject.SetActive(true); //初始化登錄注冊界面 loginWnd.InitWnd(); }); }
上面的是登錄注冊業務模塊,它直接管理的UI窗口是登錄注冊界面,要加載場景時,它會去調用資源加載服務模塊,因此資源加載服務模塊必須是單例的,加載完成后通過匿名方法傳遞委托給資源加載服務模塊,從而實現登錄注冊界面的顯示。
/**************************************************** 文件:ResSvc.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/12 22:47:38 功能:帶進度條的顯示資源加載服務模塊 *****************************************************/ using System; using UnityEngine; using UnityEngine.SceneManagement; public class ResSvc : MonoBehaviour { //單例 public static ResSvc Instance = null; //初始化模塊 public void InitSvc() { Instance = this; Debug.Log("資源加載服務模塊加載完畢..."); } Action prgCB = null; //異步加載場景方法 public void AsyncLoadScene(string sceneName,Action loaded) { //顯示加載界面 GameRoot.Instance.loadingWnd.gameObject.SetActive(true); GameRoot.Instance.loadingWnd.InitWnd(); //異步加載指定名字的場景 AsyncOperation sceneAsync = SceneManager.LoadSceneAsync(sceneName); prgCB = ()=> { //獲取當前進度 float val = sceneAsync.progress; //在加載界面設置當前進度 GameRoot.Instance.loadingWnd.SetProgress(val); //加載完成 if(val == 1) { //加載完成后調用回調函數 if(loaded != null) { loaded(); } //清空委托和中間結構 sceneAsync = null; prgCB = null; //取消對加載界面的展示 GameRoot.Instance.loadingWnd.gameObject.SetActive(false); } }; } //加載開始后,每一幀檢測進度 private void Update() { if(prgCB != null) { prgCB(); } } }
上面的是資源加載服務模塊,注意異步加載時它會通過委托實現每過一幀檢測進度,設置進度條時需要用到總管理腳本下的公用的加載進度界面,因此需要通過GameRoot去訪問,加載完成后取消對加載進度頁面的展示,同時調用傳入的回調委托,實現特定功能,比如登錄注冊業務模塊為它傳入的切換登錄注冊界面功能。
/**************************************************** 文件:LoadingWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/16 12:44:28 功能:加載進度界面 *****************************************************/ using UnityEngine; using UnityEngine.UI; public class LoadingWnd : MonoBehaviour { //Tips文字 public Text txtTips; //進度條圖片 public Image loadingFg; //進度條滑點 public Image imgPoint; //進度條文字 public Text txtPrg; //初始化加載進度界面 protected override void InitWnd() { //初始化Tips文字 txtTips.text = "這是一條游戲Tips"; //進度條歸零 loadingFg.fillAmount = 0; //初始化進度條文字 txtPrg.text = "0%"; //初始化進度條點位置 imgPoint.transform.localPosition = new Vector3(-508f, 0, 0); } //設置進度 public void SetProgress(float prg) { //設置進度條 loadingFg.fillAmount = prg; //設置進度條文字(轉換成百分比顯示,且忽略小數) txtPrg.text = (int)(prg * 100) + "%"; //設置進度條點位置(教程里設置的是recttransform里的anchoredPosition) imgPoint.transform.localPosition = new Vector3(loadingFg.rectTransform.sizeDelta.x * prg - 508f, 0,0); } }
上面的是加載進度界面的管理腳本,由於它和Unity中的一些UI關聯了,這里我們重點介紹架構,因此無需理解它里面做了什么,只需要知道它有一個自己的初始化方法和設置進度方法就行了。
/**************************************************** 文件:LoginWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/16 16:30:3 功能:登錄注冊界面 *****************************************************/ using UnityEngine; using UnityEngine.UI; public class LoginWnd : MonoBehaviour { public InputField iptAcct; public InputField iptPass; public Button btnNotice; public Button btnEnter; //初始化登錄注冊界面 public void InitWnd() { //獲取本地存儲的賬號與密碼 if(PlayerPrefs.HasKey("Acct") && PlayerPrefs.HasKey("Pass")) { iptAcct.text = PlayerPrefs.GetString("Acct"); iptPass.text = PlayerPrefs.GetString("Pass"); } else { iptAcct.text = ""; iptPass.text = ""; } } }
最后是登錄注冊界面管理腳本,同樣的,由於它和Unity中的一些UI關聯了,這里我們重點介紹架構,因此無需理解它里面做了什么,只需要知道它有一個自己的初始化方法就行了。
注意,這篇文章的重點是講述架構的優化技巧,因此我們不詳細描述異步加載進度條的具體實現,等有時間可以具體分析下代碼,但就我們馬上要記錄的架構優化而言,先熟悉下基本架構就夠了。
那么重點來了,上面這個架構有什么可以優化的地方嗎?
當然,要知道,每個界面窗口,比如上面的加載界面和登錄注冊界面,都有自己的窗口管理腳本,而當我們控制某個界面進行展示的時候,是通過其腳本獲取對此界面的引用,從而將它展示的。但是,每個界面要做的初始化工作是不一樣的,比如加載界面要將進度條置0,比如登錄注冊界面要獲取本地的賬號密碼,這些初始化工作,都是由該界面的窗口管理腳本提供的,而我們在每個地方調用並將界面顯示出來時(比如上面的例子就是在資源加載服務模塊中顯示加載界面和登錄注冊界面),都要手動地去調用管理腳本提供的初始化方法,仔細想想,其實這不大合理,畢竟我只要用到這個界面的展示,或者這個界面腳本提供的一些功能,我不應該還要負責界面的初始化工作的調用。
怎么解決這個問題?
答案就是抽取出一個窗口基類,所有界面管理腳本都繼承自這個窗口基類,將界面的初始化方法與設置界面是否激活綁定在一起,並向外部提供這個設置顯示狀態的方法,這樣,當外部(比如資源加載服務模塊)調用顯示某個界面時(比如加載界面),只要調用這個設置顯示狀態方法就行,那如何讓不同的界面管理腳本有特定的初始化方法呢?
辦法就是利用多態,將窗口基類中的初始化方法設為虛函數,在每個界面管理腳本中去覆寫這個初始化方法,而初始化方法的調用因為與設置顯示狀態的方法被綁定在一起,因此是在窗口基類中的設置顯示狀態的方法中被調用的,這樣的話這些初始化方法權限設置為protected就行了,只給子類調用,外部訪問不到它,也有保障安全性的作用。
那么,按照上面的思路,可以得到窗口基類代碼如下:
/**************************************************** 文件:WindowRoot.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/18 21:14:1 功能:UI界面基類 *****************************************************/ using UnityEngine; public class WindowRoot : MonoBehaviour { //設置界面顯示狀態 public void SetWndState(bool isActive = true) { //只有界面當前狀態和要顯示的狀態不同才要改變狀態 if(gameObject.activeSelf != isActive) { gameObject.SetActive(isActive); } //顯示后需要初始化 if(isActive) { InitWnd(); } //關閉顯示后需要清理 else { ClearWnd(); } } //初始化方法,子類中覆蓋實現 protected virtual void InitWnd() { } //清理方法,子類中覆蓋實現 protected virtual void ClearWnd() { } }
注意,這個時候,初始化方法已經被封裝起來了,在設置狀態時會自動調用,而初始化方法設置成了虛函數,方便子類進行定制。
因此加載進度界面和登錄注冊界面的代碼需要修改為:
/**************************************************** 文件:LoadingWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/16 12:44:28 功能:加載進度界面 *****************************************************/ using UnityEngine; using UnityEngine.UI; public class LoadingWnd : WindowRoot { //初始化加載進度界面 protected override void InitWnd() { //實際操作代碼 } //設置進度 public void SetProgress(float prg) { //實際操作代碼 } }
/**************************************************** 文件:LoginWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/16 16:30:3 功能:登錄注冊界面 *****************************************************/ using UnityEngine; using UnityEngine.UI; public class LoginWnd : WindowRoot { //初始化登錄注冊界面 protected override void InitWnd() { //實際操作代碼 } }
因為改動不大,因此我們僅用紅字列出了發生改動的地方,注意現在初始化方法要對父類的初始化方法覆寫,同時權限不再是公有,因為父類中權限為protected,所以子類中無法設置成public,因此此時初始化方法只能被設置狀態時自動調用,無需也不能在外部手動調用了。
此時無需改動GameRoot,但登錄注冊業務模塊和資源加載服務模塊對界面的調用也應該發生改變:
/**************************************************** 文件:ResSvc.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/12 22:47:38 功能:帶進度條的顯示資源加載服務模塊 *****************************************************/ using System; using UnityEngine; using UnityEngine.SceneManagement; public class ResSvc : MonoBehaviour { public void AsyncLoadScene(string sceneName,Action loaded) { //顯示加載界面 GameRoot.Instance.loadingWnd.SetWndState(true); //異步加載指定名字的場景 AsyncOperation sceneAsync = SceneManager.LoadSceneAsync(sceneName); prgCB = ()=> { //獲取當前進度 float val = sceneAsync.progress; //在加載界面設置當前進度 GameRoot.Instance.loadingWnd.SetProgress(val); //加載完成 if(val == 1) { //加載完成后調用回調函數 if(loaded != null) { loaded(); } //清空委托和中間結構 sceneAsync = null; prgCB = null; //取消對加載界面的展示 GameRoot.Instance.loadingWnd.SetWndState(false); } }; } }
文件:LoginSys.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/12 22:47:56 功能:登錄注冊業務模塊 *****************************************************/ using UnityEngine; public class LoginSys : MonoBehaviour { public void EnterLogin() { //加載登錄場景 ResSvc.Instance.AsyncLoadScene(Constants.SceneLogin,()=>{ //加載完成后打開登錄注冊界面 loginWnd.SetWndState(true); }); } }
同樣地,我們也省略了沒有修改的部分,用紅字標出了要修改的代碼。
你可能會說,代碼好像也不見減少了很多啊,這樣改進有意義嗎?
有的,因為其實每個窗口界面里也有可能會用到公共服務(如資源加載服務模塊),如果不作改進,那我們每個窗口界面中都要去獲取資源加載服務模塊的單例並調用功能,這樣改進之后,我們可以在基類窗口腳本中持有對此服務模塊的引用,隨后在基類初始化方法中對引用賦值,然后在每個窗口的自定義初始化函數中多調用下基類的初始化方法,就可以進一步精簡代碼,架構也更加清晰。
代碼如下:
文件:WindowRoot.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/18 21:14:1 功能:UI界面基類 *****************************************************/ using UnityEngine; public class WindowRoot : MonoBehaviour { //每個窗口都配置一些公用服務模塊 protected ResSvc resSvc = null; //設置界面顯示狀態 public void SetWndState(bool isActive = true) { //.... } //初始化方法,子類中覆蓋實現 protected virtual void InitWnd() { //初始化公用服務模塊 resSvc = ResSvc.Instance; } //清理方法,子類中覆蓋實現 protected virtual void ClearWnd() { resSvc = null; } }
/**************************************************** 文件:LoadingWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/16 12:44:28 功能:加載進度界面 *****************************************************/ using UnityEngine; using UnityEngine.UI; public class LoadingWnd : WindowRoot { //初始化加載進度界面 protected override void InitWnd() { //調用基類初始化方法來配置公共服務模塊 base.InitWnd(); //... } //設置進度 public void SetProgress(float prg) { //... }
紅字標出的就是要修改的地方,功能已經說過了,就不說了,同理,其他界面,如登錄注冊界面,也可以這么改。
完結。
