哈嘍哇,這裏是獨立遊戲開發者-月籠沙-。在上個好用工具推薦篇發佈後,收到了大家熱烈的反響,贊、收藏和關注如潮水般湧來。感謝各位,只好不辜負大家,再接再厲。
在做遊戲的過程中,寫代碼是一件很重要、很難逃開的事情,就算你用的是藍圖,也有可能遇到要寫代碼的情況。以我自己開發遊戲爲例,寫代碼,可能佔據了我總時間的60%。
足以窺見,這代碼啊,猶如在蹲廁時捧着的手機——絕對不能沒有啊啊啊!!!
除非你是美術人員(小聲嘀咕
我本身是軟件工程出身的,那麼就來分享下,代碼,應該如何寫,有哪些技巧?Warning!本篇分享,有些硬核!我會盡量通俗易懂的分享出來。由於代碼技巧篇內容有點多,我分爲上、下兩篇。
上篇:
什麼樣的代碼算是“優雅”
腳本如何分類
來,咱們寫幾個Unity中常用的工具
下篇(重中之重):
代碼的終極藝術:編程設計模式
~~~~~~
一、什麼樣的代碼算是“優雅”
當我們敲下第一行代碼的那天起,或許就已經聽過“優雅的代碼”這個概念。從那天起,你已經在不斷地努力接近“優雅”這兩個字。
那麼,到底什麼樣的代碼,算得上優雅的代碼?以我自己的理解,優雅的代碼,本質上是一種尊重——尊重你的合作伙伴,也尊重未來的自己。
我們經常吐槽“幾十個if嵌套”、“同樣的代碼複用了1145次”等寫法太“屎山”,可以看出屎山代碼具有一些特徵,反過來,我們是可以提煉出一些特徵來衡量代碼是否優雅。我總結出優雅代碼具有以下幾個特徵:
1-1 可讀性高
好的代碼像是成年人讀小學生作文,讀起來不費腦子,閒庭信步。例如,有2段語義一樣句子:
1.今天我去上學,遇到了小明,小明笑嘻嘻給了我兩巴掌。
2.於當下之日的存在場域中,主體我向教育機構進行空間位移。其間,遭遇了作爲他者的明。此他者主體呈現出一副歡愉的面部表情構型,旋即啓動了其身體暴力裝置,與我的面部區域進行了兩次突發性的、非對稱的強力接觸。
大家覺得哪一段更加好理解?在文學中,我們當然可以追求個性化的表達,抽象藝術也是藝術(bushi);但是在代碼中這麼寫,分分鐘被喊話“你來一下,我保證不打你”。寫代碼要時刻注意可讀性:
類名、變量、方法名,是否一看就知道是幹什麼用的?
![]()
代碼只因天上有,人間難得幾回聞
![]()
這下看懂了,這些是用來計算年齡的
超級if疊疊樂
如果if套的太多,使得代碼整體看上去像一個“衝擊波”,那麼可讀性是很差的。爲什麼呢?因爲在這種嵌套下,你每讀一層if,腦子裏就要多記一個條件,當你讀到4個嵌套以上,就會有點難以思考了:
![]()
真遇到這種多重判斷,該怎麼寫呢?
可以使用衛語句。大概意思就是先判斷不符合的條件,不符合則直接return,後續不執行。所以我們可以改成這樣:
![]()
是不是變得簡潔、好讀了呢?
規範的代碼命名。
在上面,我們已經知道,如果類名、變量名、方法名起得很潦草的話,可讀性會變差,所以我們要規範命名。當然沒有絕對的規範,每種語言、每個公司、每個團隊裏的規範都很有可能不一樣。所以可以不用糾結到底怎麼命名,而是將重心放在“規範”二字上。
如果你使用C#,這裏我推薦微軟官方的C#命名規範:
https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/coding-style/identifier-names
大致有2種命名法:
PascalCase(帕斯卡命名法):即每個單詞的首字母都大寫。常用在類名、方法名、屬性的命名。如果你要寫一個玩家控制器,則關鍵詞有player、controller,那麼這個控制器腳本名應該叫:PlayerController。
camelCase(駝峯命名法):第一個單詞首字母小寫,其後單詞首字母大寫,就像駱駝,從頭到駝峯,先低後高。常用在變量、方法參數的命名。例如:playerHealth。
推薦你在敲代碼的同時,網頁上掛一個翻譯軟件。當你想創建類、方法、變量時,先想一想創建出來做什麼用的,然後根據這個用處,到翻譯軟件上翻譯成英語,再利用命名法組合起來。
例如,我要創建個方法,用來返回當前場景中怪物的數量,那麼我應該去翻譯軟件上翻譯:“獲取當前場景的怪物數量”,翻譯結果爲“Get the number of monsters in the current scene”,利用PascalCase命名法,最終,這個方法名稱爲:“GetNumOfMonstersInTheCurrentScene()”。
保持代碼整潔
代碼的整潔也是很重要的,如果你像下面這樣寫代碼,真的要被親切問候了:
![]()
格式雜亂
對於書寫格式雜亂的情況,現在的IDE(代碼編輯器)都提供了一鍵整理的快捷鍵,可以將縮進、換行等進行快速整理。我們應當常用這個功能。
另外,C#中有一些預處理器指令,我非常推薦使用#region。這個指令的使用方法如下:
![]()
你可以直接摺疊這一塊region(只是被摺疊,沒有消失),使得代碼更加整潔和精煉:
![]()
1-2 分工明確
我們創建出來的類、變量名、方法,都應該分工明確,讓每個代碼單元(類、方法、模塊)都像遊戲裏的NPC一樣,只提供專屬的功能和服務。
類的單一職責原則(SRP)
理想來說,我們應當儘可能的將每個功能分配在單獨的類上。需要使用某功能時,就new出相應的類對象來使用。
方法也是如此。每個方法都應當具有獨一無二的功能。
如果你發現某一段代碼重複出現了多次,那麼應當提取出這段代碼,變成一個單獨的方法。
這樣做的好處就是,職責清晰,我該用什麼功能,都有對應的類/方法,不容易搞混;且代碼複用率高,不容易出現之前寫過了的功能還要再寫一遍的情況,非常節省時間。
1-3 易於擴展和維護
難以拓展和維護的代碼,就像嘴裏嚼到沒味的口香糖,食之無味,吐掉後變成難以忍受的環境垃圾,再無人願意多看一眼。
如何做到易於拓展和維護呢?一句話就是:高內聚、低耦合。大概意思就是,讓相關的東西更加緊密地結合在一起,讓不相關的東西儘量相互遠離。
有以下幾點可以快速提升你代碼的可拓展和維護性:
少用字符串來作爲標記,多用枚舉
字符串,作爲大部分語言裏都有的東西,它讓我們很方便記錄下一串數據。但是如果你將它作爲某種標記的話(使用字符串查找對象、使用字符串作爲條件進行比對等),是不太靠譜的。首先就是容易遺漏字母,導致結果完全錯誤;其次就是難以做到快速重命名代碼中所有用到的該字符串。
這種情況下,我們可以用其他的東西代表字符串,例如定義const(常量)字段,讓該常量等於想要的字符串,例如 const Str = "我的字符串"。然後在代碼裏需要用到該字符串時,代替爲使用該常量。
最推薦使用枚舉enum。優點是可讀性強,不會出錯,甚至暴露在Unity編輯器窗口時還能拉出選框進行選擇,非常便捷。
![]()
多使用依賴注入
什麼是依賴注入呢?其實聽起來高大上,概念卻很簡單:把需要用到的依賴,從外部讓別人送進來,而不是在內部自己創建。
依賴注入的概念我們在下篇的編程設計模式中還會講到,這裏就淺淺談一下。它的優點是解耦、方便測試。
![]()
多用接口
編程初學者可能看到接口,有點頭疼,我當初也有各種疑惑:
~接口的概念好抽象呀。
~我到底到底該不該在這裏加個接口?
~還不如直接寫個方法然後通過繼承讓子類用,單獨寫個接口多麻煩!
其實接口用多了後會發現非常多優點:降低代碼耦合度,實現 “面向抽象編程”;彌補 C# 單繼承侷限,支持 “多功能聚合”;統一規範,簡化團隊協作;便於擴展和維護,符合 “開閉原則”等。
接口也是服務於編程設計模式,所以我們在下篇還會講到。
將腳本放入合適的代碼文件夾目錄
這一部分見下文【腳本如何分類】。
1-4 健壯可靠
優雅的代碼就像大佬一樣可靠,遇到絕大多數的問題時,都能輕描淡寫,一笑而過。
怎麼才能讓自己寫的代碼,在各種條件下都能穩定運行呢?那這就必須要多寫、多實踐、多投入實際項目。這樣才能知道我這一行代碼,在真實運行環境中可能遇到什麼問題,從而完善代碼提前避免;我們也應當在寫代碼的時候,多思考:“如果是其他條件會怎樣呢?”。
二、腳本如何分類?
這個問題其實在問:我的代碼文件夾目錄結構應當是怎樣的?
有了良好的代碼目錄,添加新功能時,只需要對號入座,根據功能的分類,將該腳本放入對應的文件夾。這對於項目管理、腳本職責劃分是非常有幫助的。
在實際開發中,根據遊戲類型,代碼的文件夾可能千變萬化。不過我向你推薦一種大方向的編排:
Core
Gameplay
UI
![]()
代碼的三層文件夾架構
接下來,我向你詳細解釋,每個文件夾都是幹什麼用的,以及爲什麼要這樣劃分:
Core(核心層)
定位:項目的基礎設施和通用功能。
它相當你家裏的工具間,存放着各種工具(事件系統、存儲系統、拓展方法、Utilities等)。平常你要什麼工具,進去拿就行了。
Core層的代碼,除了自身、第三方庫、Unity底層的API外,不應當再依賴項目裏的其他代碼。就好比你家裏這個工具間,把它完整拆出來放到別人家,別人也是可以輕鬆使用的。
Gameplay(遊戲邏輯層)
定位:核心遊戲機制和玩法的實現代碼。
這個是與你實際項目結合最緊的文件夾,你玩法的具體實現(玩家有啥功能、怪物有啥功能)等,都需要放在這裏。它相當於你的家,是你真正住的地方。
UI(用戶界面層)
定位:所有用戶界面相關的邏輯。
這個就比較好理解,HUD界面、菜單系統(主菜單、暫停菜單)、對話框、彈窗、UI動畫和過渡效果、本地化文本顯示等,放在這裏就OK了。
那麼,這樣的三層代碼文件夾架構,有什麼優點呢?
方便團隊協作
這樣的三層結構,可以交由不同的人去開發,每個人可以不用管其他的層。
![]()
代碼複用性強
Core層可以在新項目中直接複用
Gameplay系統可以跨場景複用
UI組件可以模塊化複用
易於理解,拓展性強。
由於三層架構職責比較清晰,在每個層之間添加腳本,對其他層基本沒有影響,所以拓展性強。
Core、Gameplay、UI層,它們的依賴關係是如何的?
Core層:
![]()
Gameplay層:
![]()
UI層:
![]()
請務必遵循三者的相互依賴關係,否則就像宇宙的熵增:你項目最終也會變得混亂。遵循它們的依賴關係,也是一種學習如何劃分腳本職責的過程,對於編寫代碼大有裨益。
三、來,咱們寫幾個Unity中常用的工具
UpdatePublisher
強烈建議在實際項目中使用!那麼這個UpdatePublisher是何許人也?Unity裏的Update我們都知道,這個工具我們可以把它理解成Update的發佈者。
以往,我們經常在每個單獨的MonoBehaviour腳本中使用Update,這樣做很直觀,每個Update都服務於當前的腳本。
但是,由於Update每幀都執行,且每次執行都具有隱形的消耗(有人研究過Update背後的機制,Unity先會判斷幾個條件是否成立,再調用Update方法)。如果場景裏有100個腳本,那每幀都有100次Update方法的調用,那麼就有100次的隱形消耗。
你可能說,沒辦法呀,不然我每個腳本靠什麼更新呢?
欸,你別說,發明UpdatePublisher的人真是天才!假設有100個腳本,如果不使用自身的Update,而是全部調用1個腳本的Update呢?100個腳本把各自更新所需要的代碼,以委託Delegate或者事件Event的方式注入到同1個腳本的Update方法裏,讓這個單獨的Update每幀都執行這100個腳本的更新代碼,是不是就減少了99次隱形的消耗?
來看看代碼如何實現(簡單寫法,健壯性不強,大家可以自行問AI究極完全體寫法):
![]()
UpdatePublisher的簡單寫法
你只需要調用UpdatePublisher的註冊方法,將自己要每幀更新的邏輯放進去:
![]()
註冊後,即可每時每刻享受鬧鐘的“嘀嘀嘀”
對比傳統Update,UpdatePublisher的優點有非常多:
![]()
而且有非常重要的一點,普通的Class裏沒有Update方法,但是也可以通過調用UpdatePublisher來實現每幀都進行更新。
MonoBehaviour單例基類
單例是非常常用的設計模式,也是非常好用的工具,它足夠方便,但不建議大量使用。製作一個單例,可以在程序的任何地方調用。下面我向你推薦MonoBehaviour單例的一種寫法。
![]()
MonoBehaviour單例基類
如何使用它?你需要再創建一個腳本,繼承這個基類,然後將新的腳本掛在在場景裏。
![]()
繼承單例基類的Player
之後,你可以在任何地方輕鬆調用它,無需在編輯器窗口拖拽引用。例如,在怪物腳本里:
![]()
怪物攻擊時可以直接調用Player單例
遞歸查找工具
你是否經常在運行時,查找某個Transform的子物體?Unity的 Transform.Find 方法只能查找直接子物體,而無法查找間接子物體(子物體的子物體、子物體的子物體的子物體等)。
下面這種擴展方法(如果對C#的“拓展方法”概念不熟悉,可以去搜索下),能讓你查找一個Transform下面所有的子物體,無論藏得多深:
![]()
四、最後
能看到最後的朋友,相信都是對遊戲開發、編程感興趣的人,咱們一起加油。
🎶爲了心中的夢,浴血奮戰像一陣狂風🎶。
這一篇的代碼技巧,本來想着儘可能將自己所知道的都寫上去,但是發現耗費的精力太大了,還請見諒。但是沒關係,真正的經驗從來都是在實踐中獲得的,所以朋友們,動起手來吧!
在下一篇,我將探討有哪些非常優雅、非常好用的編程設計模式,敬請期待!
如果你喜歡我的分享,也可以看看我的遊戲喲
更多遊戲資訊請關註:電玩幫遊戲資訊專區
電玩幫圖文攻略 www.vgover.com
