好吧好吧,又談到這個問題了,其實早就想寫這個博客了,猶豫了好久。在設計游戲的時候我本人是很排斥什么游戲架構設計,mvc什么的,我只想馬上動手就把自己的游戲玩法最快的用代碼敲出來,還不會出無法挽回的錯誤,那么下面的步驟可以幫助你構建一個簡單的游戲模式架構。
一。首先是數據存儲類,如GameManager,UIManager,SoundManager等這些類,這些類是不銷毀的,由於這些類為物體組件,用普通的單例模式容易出現實例化的沖突,因此可以首先加載一個數據場景,間隔一段時間后加載第二個正式場景,以后最多的返回也只能返回到第二場景,這樣就解決了沖突問題
二。分場景控制類,每個場景中有可能要進行不同的復雜的控制,比如播放個動畫,選擇角色什么的。比如UI場景,該場景中主要以UI功能為主,設置音量,選擇角色什么的,都在該場景中進行,因此可以用個UIScene類的做一些特殊的控制;正式游戲場景,比如在UI場景選擇好了敵人,那么現在進入正式打擊敵人,冒險什么的,可以建立一個PlayScene場景,該場景用來控制播放個動畫次序什么的各種操作。
三。通用類的作用,比如UI控制中物體的隱藏與出現,跳轉到另一個場景的操作,銷毀物體的操作這些可以用一個GeneralController的集成這些函數,然后作為組件的形式添加到需要的分場景的場景控制的物體上,這一點尤其對UGUI的添加事件很有效。
四。觀察者類與數據存儲類的靜態變量。比如游戲加載后要把當前的語言或者音量設置到相關物體上,那么尋找攜帶這些數據的Manange類的方式有自帶的Find相關類;或者使用數據存儲類的instance靜態變量,判斷該靜態變量是否為真,如果為真,那么就說明該類的物體存在,直接調用該靜態變量獲取到相關需要的數據。因此相比較而言,使用instance靜態變量更加可靠高效
五。查找多個物體的問題,為多個物體設置數據。這里涉及到在場景中尋找具有相同標簽或者擁有相同組件的物體,這里最好的方式是為每個相同組件的物體添加一個觀察者組件,該組件用於當物體生成時,調用相關的管理類,將自己存放到管理類的存儲列表中,這樣管理類就不用在場景中使用FInd方法去尋找這些物體了。
六。對象池。這里並非這篇博客主要內容,只是做個提醒,因為拋開性能問題,上面的的五條對一般游戲來說足夠了。對一些長期生成的物體如子彈等最好做成一個對象池,從而提高機器性能。
七。托管程序。這里的托管和計算機語言的概念相類似。比如現在寫一個類,這個類執行一段程序,將物體A移動到位置p1,結束后開啟UI界面,選擇幾個物品,然后關閉UI,讓物體A移動到p2,然后制造一場爆炸,過程結束。這個過程就好像做某個任務的過程,如果寫這個過程那么移動的過程需要在update函數中執行,爆炸的過程也需要開啟,但是如果都在update中執行和判斷會導致整個程序相當臃腫。這里我們采用幾種方式:第一種,托管和事件,即將如需要每幀執行的函數,就托管給某一個類,成為這個類執行協程。第二種,狀態機與事件協同控制。在update函數中有兩個狀態標識符。state是最外層的狀態,為0時update空狀態,為1時,判斷第二個標識符號modelState,根據modelState的值開啟相應的過程,並且同時讓state的值為2或者為0,如果開啟的過程執行結束,那么可以為該開啟的過程添加一個事件,用於讓state為1,然后開啟另一個過程 。這寫過程也可以托管給其他程序執行。
===================================================================================================================================================
下面將用實例對上面的模式做出解釋,用我的游戲Recoil作為實例解釋,這里只是介紹技術,至於游戲的設計呢就不分享了,這個游戲正在做第二個版本的開發:
https://store.steampowered.com/app/844520/RECOIL/
一。數據存儲類
這里用一個場景來作為數據讀取場景,或者說這是個首頁場景,由於這三個類是單例,所以可以根據自己的需要設計。
二。分場景控制類
分場景控制類,或者可以稱為該場景下的總控類,控制該場景下的一些除了UI外的特殊必要功能,如臨時數據存儲,場景狀態控制,生成玩家,敵人等,尤其做demo的時候,這些類的作用相當高效,可以代替數據存儲類,建立測試用數據,供給UI功能或者其他功能使用;當然如果一個場景功能較多,如關卡控制,流程控制,對象池等,為了避免臃腫可以分作幾個類同時掛載在同一個物體上。
三。通用功能類設計
例如播放按鈕聲音,進行語言設置等,如果此時都放在UImanager中,那么這時在相應場景下,就需要做判斷UImanger的相關調用,並且其他場景中又有同樣的功能,所以此時可以用在不同的場景控制物體下放置同樣功能的類是很有必要的,這里可以在上面看到GeneralController類的掛載物體。
四。UI功能與事件
這里之所以將UI功能與事件搭配,這兩個概念總是息息相關。這里對這個概念的理解,要感謝給了我巨大幫助的嚴xiang,嚴大佬,學習模仿他的編碼風格和編程方式改善了我長期對游戲邏輯控制上的弊病。
首先是UI功能,比如某個UI功能模塊,這個模塊包括UI和UI對應的功能,比如一個panel下,有多個按鈕,圖片等,我們首先建立一個空物體作為該UI功能的頂級物體,空物體下放一個panel作為父物體,panel下放置其他按鈕,zipanel等,腳本掛載在頂級空物體上,腳本包含兩個最主要功能,Open和Close,最簡單如Open下panel.SetActive(false),就可以在不影響頂級物體的情況下關閉該UI界面,如果需要更復雜的關閉動畫或者打開動畫,可以在open和close下繼續添加如dotween動畫等,如果有必要可以為打開結束或者關閉結束添加相應的事件(如調用打開其他UI等)。當然這點有點像MVC控制模式,下面是一個最簡單游戲設置功能:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using UnityEngine.Audio; public class OptionPanel : MonoBehaviour { [SerializeField] GameObject panel; [SerializeField] MainPanel mainPanel; [SerializeField] AudioMixer mixer; [SerializeField] Slider musicSlider; [SerializeField] Slider effectSlider; [SerializeField] List<string> languageNames; int languageIndex; [SerializeField] Text langageText; int state = 0; private void Update() { if (state == 1) { if (Input.GetKeyDown(KeyCode.Escape)) { BackButton(); if (GeneralController.instance) GeneralController.instance.PlayClickAudio(); } } } public void Open() { panel.SetActive(true); //獲取當前音量 float valueTemp = 0; mixer.GetFloat("Music", out valueTemp); musicSlider.value = valueTemp; mixer.GetFloat("Effect", out valueTemp); effectSlider.value = valueTemp; //獲取當前語言 if (UIManager.instance) { List<string> lNames = UIManager.instance.GetLanguageNames(); if (lNames != null && lNames.Count > 0) languageNames = lNames; } string name = UIManager.instance.languageManager.GetLanguage(); languageIndex = languageNames.IndexOf(name); langageText.text = UIManager.instance.languageManager.GetCurrentLanguageLocalName(); StartCoroutine(SetState(1)); } public void Close() { panel.SetActive(false); state = 0; } IEnumerator SetState(int _state) { yield return Time.deltaTime; state = _state; yield break; } public void LeftLanguageButton() { SetLanguage(false); } public void RightLanguageButton() { SetLanguage(true); } void SetLanguage(bool isAddSet) { if (isAddSet) { languageIndex = (languageIndex + 1) % languageNames.Count; } else { languageIndex = languageIndex - 1 >= 0 ? (languageIndex - 1 <= languageNames.Count - 1 ? languageIndex - 1 : languageNames.Count - 1) : languageNames.Count - 1; } UIManager.instance.SetLanguage(languageNames[languageIndex]); langageText.text = UIManager.instance.languageManager.GetCurrentLanguageLocalName(); GeneralController.instance.UpdateLanguages(); GameManager.instance.dataSavor.language = UIManager.instance.languageManager.GetLanguage(); GameManager.instance.SaveData(); } //設置音樂音量 public void SetMusicVolume() { mixer.SetFloat("Music", musicSlider.value); if (!GameManager.instance) return; GameManager.instance.dataSavor.music = musicSlider.value; GameManager.instance.SaveData(); } //設置音效音量 public void SetEffectVolume() { mixer.SetFloat("Effect", effectSlider.value); if (!GameManager.instance) return; GameManager.instance.dataSavor.sfx = effectSlider.value; GameManager.instance.SaveData(); } public void BackButton() { Close(); mainPanel.Open(); } }
這里呢我們再看流程的控制問題,比如再改狀態下我們回到主panel的方式是直接調用mainPanel.Open()。並且為了使得按下esc按鍵也可以返回,添加了一個Update函數,但是有時候我們很不情願在一個UI功能下使用Update函數,因此我們可以將Update需要的過程都放在場景控制類下,然后每個UI界面對應場景控制類下state不同的值,這樣監測按鈕的過程都由場景控制類來進行了。比如我們關閉控制游戲設置界面的方式為界面向上移出界面,主UI界面向上移入界面,在此過程中為了使得場景控制類不能做任何操作,可以將類場景控制類的狀態為0狀態,即不能做任何操作,主界面完全移入后再通過結束事件設置場景控制類的狀態為主界面對應的狀態值。