基於自己項目的 lua 代碼規範和一些書寫的注意事項總結

目錄

一、邏輯和數據

     1、邏輯數據分離
     2、邏輯的清晰性
     3、邏輯的共性
     4、數據的聚合性
     5、數據的序列化 和 反序列化

二、常量

     1、幻數(magic number)
     2、枚舉
     3、增加枚舉類型時的可行性

三、函數

     1、函數命名的規則
     2、函數越低層,效率要求越高
     3、local 函數提高效率
     4、函數優化規則
     5、函數的長短
     6、函數複用
     7、函數調用注意事項

四、算法複雜度

     1、鍵值對替代數組
     2、減少遍歷全表的邏輯
     3、遞歸轉迭代
     4、絕對不要實現 NP 問題
     5、輪詢次數控制
     6、爲何如此美妙的代碼

五、註釋

     1、大塊註釋
     2、同步修改

六、奇技淫巧

     1、位運算加速
     2、浮點數判定
     3、減法抽象

 

一、邏輯和數據

      1、邏輯和數據分離
      到底什麼是邏輯?什麼是數據?其實沒有一個劃分的界限,只有劃分的粒度大小。簡單理解可以認爲 函數(function)就是邏輯,表(table)就是數據。那麼邏輯和數據分離要做的事情,其實就是把函數和表分開。邏輯和數據分離的好處,光寫成文字很難讓人信服,我們可以根據幾個例子來闡述邏輯數據分離的優勢。
      a) 修改數據時,邏輯可以不改。
      來看一種比較簡單的情況,這個接口是根據傳入的武學的品質返回不同的值。之前的實現如下:

       這裏的邏輯和數據糅合在了一起,會導致代碼冗長、閱讀不便、維護困難,閱讀不便的同時書寫時 bug 率就會提高,間接還會引起效率問題。那麼,我們來把這個代碼進行一下優化:

       抽出一個表叫品質表,然後把上文中品質ID的對應關係通過填在品質表的 '替代物品ID' 字段解決,邏輯代碼減少。增加新品質的時候,只需要在表中加個表項,任何涉及到品質相關的邏輯代碼都不需要修改。這個修改,把十幾行代碼優化成了兩行,再寫得極致一點只需要一行,我覺得沒必要了。所以,代碼並不是越長越好,短小精悍纔是王道。
      b) 過分拆分的函數。
      都聽過一句話,函數拆的越細越好。其實原話並不是這樣的,函數調用是有消耗的,這個消耗雖然對於邏輯來說可以忽略不計,但是當邏輯套邏輯,層層嵌套時,函數的消耗可能在最裏層,這時候就是乘法原則了。所以,函數的拆分還是不能過度。例如下面的代碼:

      一個 buff 一個函數,粗看函數調用邏輯基本一致,只是數據不同,除了沒有做到數據和邏輯分離,還把函數拆的過細,後期維護也很不方便。如果是爲了每個函數能夠清晰的指定名字,寫在數據裏即可。修改如下:

      c) 邏輯稍顯複雜,但還是有跡可循。
      邏輯很複雜,但是如果能夠寫成通用的,就能通過填表來複用邏輯,比如下面這段代碼是無法複用的:

         這段代碼是根據傳入的參數 int_params 掉落不同的物品。按照這種寫法,線上維護困難、邏輯和數據耦合太緊密容易出 bug 、功能類似的沒有抽離成數據導致代碼冗餘。基本沒有複用的可能性。
         我們可以分析一下,哪些是可變數據,哪些是固定邏輯,然後分類、建表,同時邏輯抽離。改完後如下:

首先,分析掉落數據只有金錢和物品,所以可以建立枚舉 NEWBEE_LOOT_TYPE ,然後掉落物品有可能掉落一個,也有可能掉落多個,所以可以配成列表。並且掉落之前可能觸發劇情,所以將是否觸發劇情也配置在表項中。基本就能把邏輯和數據分開了。
       除了能實現之前代碼的功能,通過不同的數據還能實現之前代碼沒有實現的功能,這就是數據驅動邏輯。
       d) 邏輯簡單,數據量巨大。
       實現隨機一個地名的函數,未優化前的版本是這樣的:

        因爲地名是個 local 的變量,每次調用函數都要生成一個大的 table,在 c 底層會有 malloc 內存分配的消耗。
        優化方法是將地名錶抽成數據表或者全局數據,每次調用函數不需要重新生成,實際測試下來,以上方法調用 10w 次的時間爲 4.2 秒;抽成數據的形式,僅需 0.07 秒。60倍的效率優化,已經稱得上是神級優化了。

       2、邏輯的清晰性
       
邏輯的清晰與不清晰直接影響到看代碼的人的心情,如果它覺得這個代碼很噁心,blame 一下發現是你寫的,那麼可能會引發一場血案。所以我們需要儘量把代碼寫得清晰易懂,下面介紹幾個優化方案:
       a) 適當換行增加可讀性
       比如以下這段正濤提供的代碼,是一個多重條件判斷,優化前的實現如下:

修改完後,雖然邏輯複雜程度還是一樣,但是起碼可讀性增加了不少,實現如下:

       b) 重複邏輯
       對於通用的邏輯,一定要寫成接口。而且,需要有一些輔助的接口文件,讓人一眼就能找到,方便調用者調用而不會去再實現一遍相同的邏輯。

       3、邏輯的共性
       
爲什麼要提出共性的概念,因爲任何事物都有共性,如果把共性提出來,可以減少很多工作量。舉個最簡單的例子,如果讓你做兩個門派:丐幫 和 華山,這兩個門派的共性是什麼? 是否有可能用相同的代碼,而通過不同的數據驅動,產生不同的邏輯?答案是一定的。通過尋找共性,精簡代碼,代碼短小精悍是永恆的主題。
       多思考,想完再寫,可以讓你減少 9/10 的代碼量,代碼量不是越多越牛逼。而是,你用 1行 就能實現別人 10 行甚至 20 行才能寫出的功能,這纔是核心競爭力!

       4、數據的聚合性
       
數據的聚合主要是指功能類似的數據儘量聚在一起,來看個例子:

        o_misc 中定義了一堆數據,有一百多個,在做數據存儲的時候很影響效率。數據定義直接導致寫邏輯的時候產生各種 if else。
        能夠歸到一類的儘量聚合到一起,比如同是聊天記錄,可以只用一個 key (聊天記錄),然後頻道在聊天記錄下層進行定義。例如:

 

       5、數據的序列化和反序列化
       序列化是將對象的狀態信息轉換爲可以存儲或傳輸的形式的過程,反序列化則是其逆過程。比如一個 table 轉化成 字符串,就是序列化,json、sproto、protobuff 都是比較常用的序列化庫。遊戲的存檔其實就是先做了序列化,然後再把序列化完的二進制數據持久化(持久化即內存數據寫到數據庫)的過程。來看一段代碼:

       通過接口獲取 '武學上限' 這個數據,發現沒有則自行創建,然後再獲取一遍。之所以獲取不到是因爲在 o_typdedef 中沒有定義。o_typdedef 是爲了數據做序列化準備的。所以需要有一個數據定義才能做這部分序列化的工作。所以,所有涉及到存盤的數據都需要在 o_typdedef 中定義數據的格式。
       之所以要邏輯和數據分離,還有一點是爲了進行序列化,序列化的一定是變的數據,即遊戲運行一段時間後函數還是這個函數,只是數據發生了變化,所以我們序列化的其實是變了的這部分數據。把函數理解爲邏輯,那麼把需要變化的數據抽離出來,然後做序列化,進行持久化,就是存檔的過程;而從磁盤上反持久化回來,再進行反序列化,得到的就是讀檔回來的數據了。
 

二、常量

      1、幻數(magic number)
       編碼者在寫一個數字的時候,當時明白是什麼含義,但是別的程序員來看的時候可能很難理解;過了一段時間之後,自己都忘了這是什麼,編碼中應該儘量避免。
       缺點:可讀性差,修改起來不方便。而且如果大量地方用到同一個數,然後某次重構沒有改全,就會引起不可預料的bug;如圖所示,圖中的 15 就是 magic number。設想一下,如果有100個文件用到這個“強化等級”,那一旦策劃將最大等級改成20級,這個修改量可想而知。
       改善:可以通過常量定義的方式修改,所有常量統一存在另一個文件中。

 

     2、枚舉
     枚舉實現的就是常量的作用,上節 幻數 中提到的就是枚舉的一種類型。來看一段代碼:

       這裏的品質直接用了中文字符串進行表示,如果日後上線要批量提升一個等級,所有的涉及到這個中文的都要修改。所以,需要抽出中間層,也就是枚舉,讓調用者直接用枚舉,在枚舉上自行定義。我們來改一下:

把數據逐步抽離後,邏輯代碼最終變成了一行,這就是重構的美妙之處。

     3、增加枚舉類型時的可行性
      遊戲中有大量用到精通的地方,如果某天老闆心血來潮加一條 XX精通,那麼所有用到精通的地方都需要增加一條對 XX精通的處理,這個是程序設計的大忌,遊戲中有大量如下圖的代碼:

引入枚舉類型的好處是,每次在增加枚舉類型時,完全不用動之前的邏輯,改善代碼以後如下:

三、函數

       1、函數命名的規則
       命名應當直觀且可拼讀,可望文知意,標識符的長度應當符合 “min-length && max-information” 原則,採用英文單詞或單詞組合,英文單詞不要複雜,但用詞需準確,切忌使用漢語拼音命名(漢語允許寫在註釋裏)
       2、函數越低層,效率要求越高
       之前做性能測試的時候發現,很多函數雖然單次調用不到 1 毫秒,但是它實現在了揹包的邏輯裏,然後角色邏輯又引用揹包邏輯,隊友邏輯又引用角色邏輯,每次嵌套都是在上層多一個循環。總的下來,卡頓就這麼出現了。所以如果你確保這個函數會被層層嵌套的時候,那麼函數的效率一定要保證。
       3、local 函數提高效率
        local 變量是存放在 lua 的堆棧裏面的是 array 操作,而全局變量是存放在 _G 中的 table 中,效率不及堆棧: 

       4、函數優化規則
       有的函數開銷比較大,而調用的頻率很低,那麼可以暫時不做優化; 反之,有的函數開銷較小,但是調用的頻率很高,從如何降低調用頻率以及減少函數開銷兩個角度去思考,然後定下優化方案。
       5、函數的長短
       函數太長會導致可讀性差,容易出錯,且查錯困難。最好拆分成可以複用的小函數。當然,函數也不宜過短,過短的函數會徒增函數調用的消耗。
       6、函數複用
       
類似的邏輯一定要抽成函數,方便調用。

       7、函數調用注意事項
       a) 避免重複調用
       
如果函數實現複雜,儘量避免重複調用,舉個例子:

這裏的函數 G.call('門派_赤刀門_赤刀下一級要求',o_item_道具) 被調用了三次,其實可以再第一次調用完畢就存到臨時變量,後面只要直接根據臨時變量的值進行判斷即可,減少函數調用的消耗要從細節做起。修改後如下:

 

四、算法複雜度

       1、鍵值對替代數組
        數組是很常用的數據結構,遊戲裏到處會用到。比如存儲玩家的成就,每次完成成就的時候判斷成就是否在 成就完成數組中,如果不在,則將它塞到數組尾部。以下是判斷成就是否完成的實現:

        這樣的實現,最壞時間複雜度 O(n)。數組長度是2000,就要遍歷 2000 次。如果在客戶端只有自己在跑,影響不會很大,但是在服務器上,每個玩家都有這個邏輯,最壞情況是 2000 乘上 同時玩家數,會造成服務器的阻塞。
        lua 的 table 除了數組的功能,還能實現鍵值對, 底層實現是哈希表。哈希表的插入、刪除都是 O(1) 的。

 

        2、減少遍歷全表的邏輯
        有些時候需要遍歷整個物品表,篩選出符合條件的物品。當表非常大時,遍歷非常耗 CPU。

我們可以在邏輯功能需要篩選某些物品時,把候選的物品 ID 填在數據表的字段裏,然後再在候選的 ID 中進行篩選。

      3、遞歸轉迭代
      
遞歸的代碼一般涉及到函數的系統調用棧,所以會比較耗,很多遞歸都能改成 while 迭代。來看一個求兩個數的最大公約數的實現。任何簡單的遞歸都能通過一定的方式轉換成迭代,只不過需要引用一個棧的數據結構去做這件事情。

      4、絕對不要實現 NP 問題
      NP問題即沒有多項式算法的問題,這種算法是窮舉(例如有向圖的深度優先搜索),很可能一輩子都跑不出結果。類似的實現還有隨機一種情況,如果條件滿足就跳出;條件不滿足繼續隨機。總之,算法複雜度無法預估的算法都是耍流氓!!!

      5、輪詢次數控制
      當函數輪詢超過 100 次時需要加以關注;
      當函數輪詢超過 1000 次時需要思考算法可行性;
      當函數輪詢超過 10000 次時需要提高警惕,已經不是算法本身的問題了;
      當函數輪詢超過 100000 次時需要殺個程序員祭天;
      6、爲何如此美妙的代碼

        判斷一個實例是否某個列表中的其中一個,這麼寫是完全沒必要的。
        首先,邏輯運算_或 這個函數的參數是個大的 table,需要等 table 構建完畢纔會執行函數邏輯,而邏輯或的運算是一個條件成功就返回了,那麼這個構建 table 的開銷浪費掉了。
        再者,這裏可以用一種很簡單的方法,把滿足條件的屬性設置到 o_clan_門派 本身,作爲它的一個屬性,這樣就可以控制在表裏了,代碼邏輯也會很簡單,如下:

 

五、註釋

     1、大塊註釋
     
非常用邏輯、複雜算法、難懂的代碼 等需要大塊註釋說明,這個主要是要養成一個意識。

     2、同步修改
     
重構完代碼,需要將註釋修改或者刪除,這個往往容易忘記。如下,增加了隨機類型,但是註釋沒有改:

六、奇技淫巧

     1、位運算加速
         1) 加速運行效率
         通過字符串拼接生成一個十六進制的數,再轉化成十進制。實現如下:


         上面的問題是先轉成string,完成字符串拼接,再轉回整數。完全可以用位運算實現:

一些位運算的技巧:http://graphics.stanford.edu/~seander/bithacks.html

 

        2) 縮短代碼實現
        舉個 '根據 武學品質 獲得 武學等級' 的例子,代碼實現如下:

         以上代碼除了可以將 武學品質 單獨建表實現功能外,還可以有一種奇技淫巧,就是位運算的 & 代替 %。

 

     2、浮點數判定
     
浮點數的存儲不同於整型,它存的是指數和階數。在判相等的時候不能單純的用 == ,來看一個例子:

這就是常說的浮點數精度誤差,誤差時刻存在,所以在判定的時候我們需要一些技巧,比如將兩個數相減,如果差的絕對值小於一個足夠小的數就認爲他們相等:

 

     3、減法抽象
     
兩個數字的比較邏輯,可以轉換成數字相減,然後用結果當條件去做接下來的處理。
     舉個例子,這個是剪刀石頭布的邏輯:

抽象後,可以把剪刀石頭布的選擇用整數表示,然後就是做整數減法,最後用映射的方式做跳轉邏輯。轉換後的代碼很短,如下:

RPS_STRING 爲 '剪刀石頭布' 的枚舉,RPS_RLT 代表 '剪刀石頭布' 輸贏的枚舉,RPS_RLT_MAP 的 key 代表兩者相減的結果,value 處理輸贏,RPS_STORY 用來做輸贏時的分支劇情。數據常量如下:

 

最重要滴、事不過三,三則重構
         發現一個邏輯反覆需要,但是一直都在重複寫代碼,說明你需要重構你的代碼了。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章