1. 程式人生 > >涼鞋:我所理解的框架 【Unity 遊戲框架搭建】

涼鞋:我所理解的框架 【Unity 遊戲框架搭建】

前言

架構和框架這些概念聽起來很遙遠,讓很多初學者不明覺厲。會產生“等自己技術牛逼了再去做架構或者搭建框架”這樣的想法。在這裡筆者可以很肯定地告訴大家,初學者是完全可以去做這些事情的。

初識架構和框架

架構和框架是非常接地氣的,離我們其實並不遙遠。

什麼是架構?

架構是一個約定,一個規則,一個大家都懂得遵守的共識。那這是什麼樣的約定、什麼樣的規則、什麼樣的共識呢?

我以包為例,我經常出差,雙肩揹包裡裝了不少東西。膝上型電腦、電源、2 個上網絡卡、滑鼠、USB 線、一盒大的名片、一盒小的名片、口香糖、Mini-DisplayPort 轉 VGA 介面、U 盤、幾根筆、小螺絲刀、洗漱用品、乾淨衣服、襪子、香水、老婆給我帶的抹臉膏(她嫌我最近累,臉有點黃)、錢包、Token 卡、耳機、紙巾、USB 線、U 盤等。這個包有很多格子,最外面的格子我放常用的,比如筆、紙、一盒小的名片等;中間的格子一般放的是衣服、襪子、洗漱用品、香水等;靠背的那個大格子放了膝上型電腦,和膝上型電腦相近的小格子放的是兩個上網絡卡、Mini-DisplayPort 轉 VGA 介面、大盒名片、記事本,和膝上型電腦相近的大格子放的是電源、滑鼠、口香糖等。

我閉著眼睛都可以將我的東西從包裡掏出來,閉著眼睛都可以將東西塞到包裡!但是,非常不幸的是,一旦我老婆整理過我的包,那我就很慘了,老是因為找不到東西而變得抓狂!更不幸的,要是我那個不到兩歲的“小可愛”翻過,就更不得了了。

這個包就是我放所有物品的“架構”,每一個東西放置的位置就是我的“約定、規則、共識”。倘若我老婆也知道我的“架構”、我的“約定、規則、共識”,那麼不管她怎麼動我的包,我都照樣能夠輕易的拿東西或者放東西。進一步,如果我的同事也知道我的“架構”,知道我的“約定、規則、共識”,那麼他們什麼時候動我的包,我也毫無所知!——道法自然 《10 年感觸:架構是什麼?——消滅架構!》

什麼是框架?

框架(framework)是一個框子--指其約束性,也是一個架子--指其支撐性。——360 百科

小結

本小節對框架和架構概念做了簡單的認識,得出了以下兩個結論:

  • 架構是“約定、規則、共識”
  • 框架具有約束性和支撐性

到這裡,大家應該對這兩個概念有點感覺了。但是還是會有很多疑問,比如“如何去做架構?”、“框架的約束性和支撐性分別指的什麼?”等等,沒關係,筆者堅信“帶著問題去閱讀”往往是最有效的閱讀方式。接下來筆者將分享這兩年來對框架和架構探索的經歷,以及對這兩個概念認識的演變,希望給大家帶來一些啟發,順便大家心中的一些問題得到解答。


QFramework

兩年前,筆者畢業半年,剛從 cocos2d 轉 Unity 不到兩個月,當時所在的公司有一套遊戲開發框架。筆者用它做了兩個月的專案,使用框架做專案的時候並沒有去思考框架是什麼,只是開始的時候覺得很新鮮,而且越用越順手,嚐到了它的甜頭。

後來筆者接到了一個跑酷遊戲專案,於是就把工作辭掉了,決定出來全職做這個專案。辭職後,公司的框架由於保密協議就不可以用了。專案就只能從零開始開發,那麼結果就是在跑酷專案的開發的過程中各種中水土不服。

於是,筆者就開始了市面上開源框架的選型,折騰了幾天,發現要麼上手太難,要麼學習成本很高文件不齊全,有的框架光是理解概念就要很久,對於像筆者一樣剛畢業的初學者來說,市面上的開源框架真的很不友好。

從那時候筆者就決定要 為自己,開發一套符合自己使用習慣的框架,也就是現在的 QFramework。

為什麼叫 QFramework ?

筆者在做 cocos2dx 的時候,市面上有個叫 Quick-Cocos2d-x 的開源框架,用兩個詞形容就是簡單、強大。

筆者認為好的工具就應該簡單。

QFramework 的目標是要做到像 Quick-Cocos2d-x 一樣 “簡單、強大”。當時筆者糾結過很多名字,比如 QuickEngine,QuickUnity 等等。Q 代表 Quick,並且 Q 這個字母給人感覺靈活有彈性,所以最終確定為 QFramework。

在決定要做框架之後,筆者就開始了邊搭建框架邊進行著跑酷專案開發的工作生活。

啟蒙資料 《Unity 架構設計與開發管理》

很幸運地是,在跑酷專案開發之初,筆者接觸到了一個非常好的關於 Unity 專案架構的學習資料,就是劉鋼老師在 Unite 大會上的講座視訊《Unity 架構設計與開發管理》,視訊中所提出的 Manager Of Managers 很好地為筆者開發 QFramework 指明瞭方向。雖然劉老師講得通俗易懂,但是裡邊有很多話都很值得回味,筆者之後也花了很長時間去消化裡邊的內容,直到今天,筆者再看一遍視訊還是會有很多收穫的。在讀此文時我們先不著急看裡邊的內容,視訊連結會在文章的最末尾貼出。

跑酷專案準備

一個專案開始立項的時候,最常見的一個情況就是:幾個人一個小團隊,開始什麼也不做,開始寫程式碼,驗證邏輯,然後 game 就開始寫起來了。公司的一些的所謂的領導層一開始就把遊戲定義為“我們要做的一個大作”,那麼這個事情本身就是一個笑話。沒有任何的規劃和設計,我們就妄圖就寫出一個所謂的傑出的作品出來是不現實的。Unity 再好用,以這個心態去做遊戲,一定會寫不出來好的遊戲來。——劉鋼《Unity 專案架構和開發管理》

看到視訊中的這段話,嚇得筆者趕緊為跑酷專案做了些準備。比如最常見的表現和邏輯的分離。

表現邏輯分離

我們大家都知道,做專案儘可能地要把表現和邏輯分離。同樣的跑酷專案也是如此,而最常用最經典的方式就是使用 MVC。

經典的 MVC 架構模式

MVC 是在軟體開發時最常用的架構模式,我想大家都應該接觸過,所以 MVC 的概念在這裡筆者不會浪費口舌去贅述,不瞭解的同學可以參閱阮一峰前輩的《談談MVC模式》。

跑酷專案中的 MVC

跑酷專案程式碼的架構使用的是很簡單的 MVC。筆者當時是按照如下的方式去進行劃分的:

  • View 層是各個 UI 的 Transform 本身。
  • Ctrl 層則是掛在各個 View GameObject 上的 MonoBehaviour。
  • Model 層則是若干個提供資料增刪改查的並且可以全域性訪問的單例。

跑酷的 MVC 架構圖如下所示:

從今天的來看,這種 MVC 設計是一種很粗糙的設計,尤其是其 Model 層,顆粒度太大,其實再可以分出個 DataAccess 層。不過粗糙的好處就是初學者能夠駕馭,是有存在的意義的。

到這裡,架構這個詞終於出現了。MVC 是一種架構模式,對程式進行 MVC 的劃分是在進行架構活動。除了 MVC 架構模式,還有幾種其他的架構模式。

而對程式進行 MVC 的劃分,實際上是對程式碼進行結構的設計,所以對程式進行結構的設計是在進行架構活動。

到這裡,我們知道了架構是我們每天都在做的事情(劃分 MVC 或者說將程式碼的表現與邏輯層分離)。而對程式碼進行結構的設是否和架構的“約定、規則、共識”有關係呢?答案是肯定的。我們接著往下進行探索。

檔案結構

很多時候我們說的一個所謂的好的架構,直接就等於你要有一個好的標準,指定一些好的規則。——劉鋼《Unity 架構設計與開發管理》

Unity 好的規則 3:

我們在起一些資料夾的名字的時候,儘量和我們的 GameObject 對應起來,如果我們的 GameObject 叫做 PoolManager,我下面所有的程式碼也都起一個同樣的名字叫 PoolManager,那麼這樣在以後找對應的程式結構的時候,會比較好理解一些。——劉鋼《Unity 架構設計與開發管理》

而跑酷專案最初的部分程式碼檔案結構如下:

  • Scripts
    • App
      • Objects
        • Stages // 關卡單元
          • XXStage.cs
          • ...
        • Enemy // 敵人
          • EnemyCtrl.cs
          • EnemyModel.cs
        • Player // 角色
          • PlayerCtrl.cs
          • PlayerModel.cs
        • ...
      • Managers
      • EnemyManager.cs * StageManager.cs
      • ...

對於 MVC 的檔案結構的劃分,筆者在進行跑酷專案之前的其他專案的時候也嘗試了別的方案。當時覺得這種方式找起來最方便。

在專案結束之後不久看到了一篇吳秦前輩的一篇好文《Unity3D手遊開發實踐》:

一般客戶端用得比較多的 MVC 框架,怎麼劃分目錄?

先按業務功能劃分,再按照 MVC 來劃分。“蛋糕心語”就是使用的這種方式。

先按MVC劃分,再按照業務功能劃分。“D9”、“寶寶鬥場”、“魔法花園”、“騰訊桌球”、“歡樂麻將”使用的這種方式。

根據使用習慣,可以自行選擇。個人推薦“先按業務功能劃分,再按照 MVC 來劃分”,使得模組更聚焦(高內聚),第二種方式用多了發現隨著專案的運營模組增多,沒有第一種那麼好維護。——吳秦《Unity3D手遊開發實踐》

而筆者所採用的方式就是“先按照業務功能劃分,再按照 MVC 來劃分”。在這裡僅僅是個建議,並不是一定要使用的方式。

QFramework 的第一個工具“單例模板”

在跑酷專案的 MVC 中,筆者把 Model 層設計成了單例。為什麼使用單例呢?

拋開粗糙的設計不說,先讓我們簡單分析下跑酷專案的 Model 層的特點:

  • Model 層的資料在整個軟體的生命週期裡只有一份。
  • Model 層的資料要對任何 Ctrl 都提供增刪改查。

很自然地就想到了使用單例模式實現。而很多開源庫裡都會提供可複用的單例模板,一般叫 Singleton 和 MonoSingleton。所以單例的模板作為第一個工具被收錄到框架中。

“程式碼是資產”思維

在寫一個專案的時候,不要短視的說我就把這個專案做完了,就是交一個差上線了就完了,我們希望每寫一個遊戲的時候,我們都積累一些東西,把寫的每一行程式碼,都當成是一個可以收藏的,甚至是可以傳遞下去的這樣的一個資產。有了這樣一個思想,可能我們在寫程式碼的時候,整個的思維模式會完全不一樣。——劉鋼《Unity 專案架構和開發管理》

聽到上面這段話,在當時的筆者心中埋下了一顆種子。在這個基礎上進行思考,會產生很多很有價值的想法。本小節先講到這裡。

小結

文章讀到這裡我們簡單進行總結一下。

  • 架構是“約定、規則、共識”
  • 框架具有約束性和支撐性
  • 好的架構直接就等於一套好的規則,好的準則
  • Unity 好的規則
    • 3.起資料夾的名字時儘量和 GameObject 對應起來
    • MVC 檔案結構:先按照業務功能劃分,再按照 MVC 來劃分
  • “程式碼是資產”思維
  • QFramework
    • 工具集
      • FSM
      • Singleton&MonoSingleton
      • MsgDispatcher
      • MathUtils

結合跑酷專案準備階段,以上的結論應用的結果如下圖所示:

圖中的需求收集/業務分析是本小節沒有講的,由於這個跑酷專案是合作專案,有一些需求合作方本身也沒太想清楚,在進行準備這個階段之前,跑酷專案已經完成了一版 Demo 進行了需求上的確認。

由於受到“程式碼是資產”思維的影響,QFramework 的開發模式最初是以收集工具為目的的,此時的 QFramework 並不是真正意義上的框架,而是一個庫或一個工具集。

跑酷專案開發

在準備完畢之後,跑酷專案就開始了大量的業務/邏輯開發。

語言學習

在做跑酷專案時,筆者當時的水平怎麼樣呢?三個字,非常菜。有很多很基本的功能要學習 Unity API 才能完成,比如跑酷的關卡生成器等等。對 C# 語言的掌握也是靠著以前一點 Java 經驗,才能勉強能應付邏輯開發,之前所說的單例的模板,也只是知道怎麼去用,並不知道實現的原理。這時候筆者覺得必須要對 C# 進行基礎學習。於是就開始每天看一點傳智的 C# 基礎視訊。學習的過程中,一些語言特性不知道怎麼用,而有的語言特性覺得很有用。所以此時只是為了完成專案而進行學習,自然而然地就沒有太多的精力去深究語法細節。我們大家都知道,這不是一個很好的學習方式。

QFramework 是知識積累工具

隨著對 C# 語言的瞭解程度加深,慢慢地可以看懂一些工具的原始碼了,也可以自己實現一些很簡單的封裝。而筆者在跑酷專案的開發期間先後收錄了有限狀態機、訊息分發器和一些數學工具。以上收集的工具與單例的模板一樣,都是同一性質的工具,所以這裡沒什麼好說的。值得一提的是筆者當時做了一件事,筆者按照之前 cocos2dx 的使用習慣把一些 Unity 的 API 簡單封裝了一下,最初這麼做只是為了提高自己的開發效率,擴大自己在 Unity 裡的舒適區(筆者之二前一種用 cocos2d)。做了這件事之後給了筆者很大啟發,筆者為什麼不把一些新學習的 Unity 的 API 或者 C# 特性簡單封裝一下然後收錄到 QFramework 中呢?這樣以後使用這些 API 的時候就不用再查詢搜尋引擎了,直接使用封裝的工具就好了。這樣還能讓 QFramework 幫助筆者“記住” Unity 的 API 和 C# 的特性。從那以後 QFramework 不止是一個工具集,也是筆者的一個知識積累工具。這樣耗能解決上文中筆者對“學習的知識沒有用武之地”的困擾。這樣既能激發筆者的學習動力,又對 QFramework 本身也有好處,一石二鳥。

業務支撐工具

我們都知道,做一個遊戲專案,都會用到 UI、音效、配置表和資料儲存等模組。跑酷專案也是一樣的,在劉鋼老師的《Unity 架構設計與開發管理》視訊裡提出了一個叫做 Manager Of Managers 的架構方案,可以把以上模組全部做成一個單例,比如 UIManager,AudioManager 等。而筆者認為,這些工具模組都是為了支撐遊戲業務的,比如遊戲音訊管理方案,介面層級管理方案等等。也就是說大多數專案都用得到。而不像單例的模板、有限狀態這些工具,它們不是為了支撐業務而積累的。為什麼這麼說呢?單例的模板是設計工具,解決的問題不是業務問題是設計問題,而有限狀態機則是一種資料結構,是簡化一部分問題的思維模型。而 UIManager、AudioManager 等等。每個模組都是獨立的解決方案,是為了解決某一業務問題而設計的。所以筆者在這裡稱它們為業務支撐工具。

而劉鋼老師的在視訊中列出了以下模組:

  • MainManager
  • EventManager
  • AudioManager
  • GUIManager
  • PoolManager
  • LevelManager
  • GameManager
  • SaveManager
  • MenuManager

這裡除了 GameManager 以外,其他的全部可以在別的專案中複用。

這時跑酷專案中已經實現了

  • SoundManager(AudioManager)
  • ConfigManager(LevelManager)

這兩個工具自然也收錄到了 QFramework 中。這樣 QFramework 的目標有了一些變化。而 QFramework 除了是工具集和知識積累工具之外,還是一個支撐業務工具的集合。而之後筆者要做的就是把就是 Manager Of Managers 提出的這些模組一一收錄。

專案結束

很快,跑酷專案接近尾聲。拿到結款後分析了一下當時工作生活狀態的利弊。決定還是找一家公司繼續沉澱。一是為了讓 QFramework 多接觸一些不一樣的專案,二是筆者非常渴望與同行能交流的,三是開發跑酷專案的這兩個半月對未知的恐懼太強烈了,比如對未掌握的技術,沒把握實現的功能等等,四是合作方的第二個專案需要組個完整團隊去做,當時身邊沒有太合適的人。總之跑酷這個專案到此完美結束了。

小結

在文章的最開始,筆者提出了框架具有約束性和支撐性。 QFramework 目前已經具備了其中的支撐性,也就是支撐業務。而業務是其中一種框架所能支撐的領域,除此之外還有其他的領域,比如團隊協作,工作流等等,這裡先不多說。而約束性到目前還沒有提過。沒關係,我們接著往下進行。

文章讀到這裡我們再進行總結一下。

  • 架構是“約定、規則、共識”
  • 框架具有約束性和支撐性
  • 好的架構直接就等於你要有一套好的規則,好的準則
  • Unity 好的規則
    • 3.起資料夾的名字時儘量和 GameObject 對應起來
    • MVC 檔案結構:先按照業務功能劃分,再按照 MVC 來劃分
  • 程式碼是資產
    • 做完每一個專案都積累一些東西
  • QFramework
    • 工具集
      • FSM
      • Singleton&MonoSingleton
      • MsgDispatcher
      • MathUtils
    • 筆者知識積累工具
    • 業務支撐工具集(支撐性)
      • SoundManager
      • ConfigManager

新的公司

2016 年 3 月下旬,跑酷專案做完之後來到了一家新的公司,來的時候公司已經具有一定的規模,其遊戲技術團隊也積累了一些 Unity 的外掛和工具。而筆者所加入的團隊是技術支援團隊。技術支援的工作就是平時負責攻克技術難點,做一些預研,再做一些工具來給專案團隊使用,有的時候專案人手不夠了還要頂上去。而在這家公司的兩年則是筆者成長最快的兩年,一是遇到了好 Leader,二是做的事情非常喜歡。

UIManager

到了新公司,工作的同時,業餘時間 QFramework 的開發還是要進行的。而公司的專案是沒有一個很統一的框架,每個專案組都是各幹各的。但是我們部門的好處就是都可以看到專案組的程式碼。每個專案的 UIManager 都是基於 Dictionary 來提供查詢,然後簡單地用 GameObject 的前後關係來管理 UI 的層級。筆者當時也看了一些市面上開源的 UI 框架,實現原理都差不多。所以索性就自己開發了一套很平庸的實現,沒有什麼太大的亮點。不過好在,算是完成了對 ManagerOfManagers 中 GUIManager 的收集。

AssetBundleManager

AssetBundleManager 是看一個公司專案時候看到的,本身是一個開源免費的 Asset Store 外掛。筆者之前對於資源管理沒有太大的概念。像之前做的跑酷專案,都是直接都把 GameObject 拖到場景裡完成的。很少用到動態載入解除安裝記憶體。但是看了 AssetBundleManager 之後,很看好它的 Simulation Mode。所以就收錄到 QFramework 裡了。使用 AssetBundle 的好處有很多,支援熱更啊,控制包體大小啊等等。缺點就是坑多,而且有一些學習成本,但是還是非常值得去研究的。

加班

在收集了 UIManager 和 AssetBundleManager 之後開始了時長較長的加班。加班的原因就不說了,從這時候開始 QFramework 就擱置了一段時間。

小結

本小節的 UIManager 和 AssetBundleManager 都是一種很普通的實現,沒有太大的亮點。

文章讀到這裡我們再進行總結一下。

  • 架構是“約定、規則、共識”
  • 框架具有約束性和支撐性
  • 好的架構直接就等於你要有一套好的規則,好的準則
  • Unity 好的規則
    • 3.起資料夾的名字時儘量和 GameObject 對應起來
    • MVC 檔案結構:先按照業務功能劃分,再按照 MVC 來劃分
  • 程式碼是資產
    • 做完每一個專案都積累一些東西
  • QFramework
    • 工具集
      • FSM
      • Singleton&MonoSingleton
      • MsgDispatcher
      • MathUtils
    • 筆者知識積累工具
    • 業務支撐工具集(支撐性)
      • UIManager
      • AssetBundleManager
      • SoundManager
      • ConfigManager

命名的力量

Unity 好的規則 2:我們在命名的時候要起一個比較有含義的名字。——劉鋼《Unity 架構設計與開發管理》

在筆者剛畢業的時候,讀了《程式碼大全》這一本書,其中第 11 章的《變數名的力量》反覆讀了三遍。這一章的內容對筆者之後養成良好的命名習慣產生很大的影響。書中重點講了如何命名和編碼規範的重要性。

編碼規範

筆者很認同做專案要遵循命名規範這個準則的。但是現實是規範的執行過程中會遇到很多阻礙。比如筆者最開始去網上找了一個編碼規範文件。一份文件 50 多頁,看都覺得很痛苦,只好放棄。最終經過一段時間探索之後得出了一個結論:編碼規範只要解決問題就好,其他的儘量確保容易遵守就好。

編碼規範解決的是什麼問題呢?

  1. 減少專案交接時,由於程式碼風格水土不服所帶來的風險。
  2. 當某個專案人力不足時,可以減少加人時所帶來的人力浪費(可以讓一個人花更少的時間去看懂某個專案的程式碼)。
  3. 防止專案過了一段時間一些實現自己都看不懂了。

以上當然要靠一個編碼規範是無法完全解決的,除了編碼規範之外,還有資源命名規範,專案結構規範等等。
經過多次因為以上原因的加班之後,深有感觸。編碼規範是非常有必要做的。

為自己制定一個編碼規範

在公司,筆者還是最基礎的員工,沒有什麼權利。所以對於定義規範這種事情,在公司想想就好了。不過這並不阻礙筆者為自己制定一個編碼規範。於是筆者根據自己的編碼習慣,定製瞭如下的編碼規範。

/****************************************************************************
 * Copyright (c) 2017 liangxieq
 * 
 * https://github.com/liangxiegame/QCSharpStyleGuide
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ****************************************************************************/
 

namespace QFramework.Example 
{   
    using UnityEngine;
    using UnityEngine.UI;
    using System.Collections;
    using System.Collections.Generic;
    
    /// <summary>
    /// 展示編碼風格
    /// </summary>
    public class ProgrammingStyle : MonoBehaviour 
    {
        #region Basic App
        /// <summary>
        /// 1.private/protected使用m開頭+駝峰式
        /// 2.字首最好展示所屬的Component型別比如Button->Btn
        /// </summary>
        [SerializeField] Button mBtnEnterMainPage;

        /// <summary>
        /// public型別使用首字母大寫駝峰式
        /// </summary>
        public int LastIndex = 0;

        /// <summary>
        /// public 型別屬性也算public型別變數
        /// </summary>
        public int CurSelectIndex 
        {
            get { return mCurSelectIndex; }
        }

        void Start () 
        {
            mBtnEnterMainPage = transform.Find ("BtnEnterMainPage").GetComponent<Button>();

            // GameObject命名
            // 臨時變數命名採用首字母小寫駝峰式
            GameObject firstPosGo = transform.Find ("FirstPosGo").gameObject;
        }

        /// <summary>
        /// 方法名一律首字母大寫駝峰式
        /// </summary>
        public void Hide() 
        {
            gameObject.SetActive (false);
        }
        #endregion

        #region Advanced
        /*
         * GameObject->Go
         * Transform->Trans
         * Button->Btn
         * 
         * For->4
         * To->2
         * Dictionary->Dict
         * Number->Num
         * Current->Cur
         */

        /// <summary>
        /// 1.Bg肯定是圖片
        /// </summary>
        [SerializeField] Image mBg;

        /// <summary>
        /// GameObject->Go
        /// </summary>
        [SerializeField] GameObject mDialogGo;

        /// <summary>
        /// Transfom->Trans
        /// </summary>
        [SerializeField] Transform mScrollViewTrans;

        /// <summary>
        /// Index、Num、Count等肯定是int
        /// </summary>
        [SerializeField] int mCurSelectIndex;

        /// <summary>
        /// RectTransform->RectTrans;
        /// </summary>
        [SerializeField] RectTransform mScrollContentRectTrans;

        /// <summary>
        /// 1.Pos肯定是Vector3、Vector2
        /// 2.Size肯定是Vector2
        /// </summary>
        [SerializeField] Vector3 mCachedPos;
        [SerializeField] Vector2 mCachedSize;

        /// <summary>
        /// 字尾s表示是個陣列
        /// </summary>
        [SerializeField] Vector3[] mCachedPositions;

        /// <summary>
        /// 1.List字尾
        /// 2.4->for 表示所屬關係可以表示Dict
        /// 3.Dict後置
        /// </summary>
        [SerializeField] List<Vector3> mCachedPosList;
        [SerializeField] Dictionary<string,Vector3> mPos4ChildName;
        [SerializeField] Dictionary<string,Vector3> mChildPosDict;
        #endregion
    }
}

規範直接使用程式碼展示,容易看懂自然而然也就會容易遵循。

破窗效應

一幢有少許破窗的建築為例,如果那些窗不被修理好,可能將會有破壞者破壞更多的窗戶。最終他們甚至會闖入建築內,如果發現無人居住,也許就在那裡定居或者縱火。一面牆,如果出現一些塗鴉沒有被清洗掉,很快的,牆上就佈滿了亂七八糟、不堪入目的東西;一條人行道有些許紙屑,不久後就會有更多垃圾,最終人們會視若理所當然地將垃圾順手丟棄在地上。這個現象,就是犯罪心理學中的破窗效應。

我們做專案也是一樣的。一定要好好寫程式碼,不要讓“破窗”在我們的專案中發生,不能讓專案有任何變混亂的趨勢,保持專案清爽,這可以給我們開發者到來很好的工作體驗,也就是所謂的心流體驗。

如何命名?

在一些命名格式上,可以遵循編碼規範就好了。但是如何給一個類/方法/變數/列舉命名呢?

在問這個問題前,我們來問另外一個問題,那就是程式語言,所謂的語言是給誰看的?一是給計算機或者編譯器能看懂。二是給我們人類看的。讓計算器或者編譯器看懂很容易,只要遵循程式的語法去寫就 OK 了。但是如何讓人更容易看懂,當然答案也很簡單,就是好好命名。關於如何命名,一些筆者至今受用的命名準則這裡這裡簡單介紹下。

  • 使用業務相關的詞彙命名而不是計算機相關的詞彙

比如,SaveMgr 中的 Save 是儲存,是業務相關的詞彙。而 SerializeHelper 中的 Serialize 則是計算機相關的詞彙。這條準則在 業務/邏輯/UI 層會有很大的效果。而在 Framework 層或者說底層還是使用計算機相關的詞彙比較好。

  • 方法/函式命名用謂語 + 賓語方式命名
    比如 PlayerData.Save,或者 SavePlayerData

  • 類名和方法引數使用名詞
  • 表示一個動作狀態時通過動詞的不同時態進行命名。

比如 Connecting,Connected,Connect 表示連線的三種狀態。

關於命名和規範就先講到這裡,命名是一門學問,其內容多得可以去寫一本書去介紹了。如果想深入學習,建議首先看《程式碼大全》的第 11 章 《命名的力量》。

小結

還記得在前邊說的架構的定義嘛?架構是“約定、規則、共識”,而確定各種規範也是準備階段要做的事情,也是架構的一部分。

再次起航

在新的公司度過了一段加班生活,之後加班次數慢慢就減少了。這時候就又有時間去搞點東西了。

一個視訊教程的學習

首先是當時,在某教育網站上學習了《萬能遊戲框架》視訊教程,筆者從頭到尾跟著手敲了一遍。教程裡的一個基於模組的訊息框架實現得很有意思。這裡簡單說一下。 QFramework 之前收錄的 MsgDispatcher 就是一個全域性的字典,字典的 key 是事件名字,而 value 則是 委託 List,所以不管怎麼定義訊息,它們都是全域性的。當訊息的規模變大之後,會有很大的效能壓力。如圖所示:

全域性訊息與單例模式一樣都是用起來很方便,但是風險很大的設計。

而 《萬能遊戲框架》裡的訊息則是以模組為單位的。比如 UI 模組則只負責 UI 介面相關的訊息收發和註冊,Audio 模組同理也是。 而跨模組之間則用一個簡單的 switch 進行轉發。比較出彩的是其中的關於頻段的設計。我們都知道 C# 裡的 ushort 的最大值是 65536,視訊中每個模組的頻段長度設為 3000,這樣最多可以有 21 個 模組,足夠使用了。每個模組可以註冊 3000 個訊息。如何實現,這裡看下程式碼就明白了。

public enum MgrId
{
    UI = 0 * 3000,
    Audio = 1 * 3000,
    ...
}

public enum UIXXEvent
{
    Start = (ushort)MgrId.UI,
    XX,
    YY,
    End,
}

public enum UIYYEvent
{
    Start = (ushort)UIXXEvent.End,
    ZZ,
    End,
}

筆者當時看到這裡才覺得自己對語言的瞭解真的是很淺,一個簡單的 ushort + 列舉就可以很巧妙地設計出基於模組的訊息框架,這種思想非常值得借鑑。筆者馬上在 QFramework 中實現了一套類似的訊息框架。很簡單,一個 QMsgCenter 充當跨模組之間的訊息轉發。一個 QMgrBehaviour 作為模組的基類,負責收發和註冊模組內的訊息,一個 QMonoBehaviour 只要一個指令碼繼承它,就可以傳送訊息和註冊處理訊息。而事實上,有了這套訊息框架,QFramework 才算是一個真正的 Manager Of Managers 框架。

初涉工作流

公司的以為前輩也有一套類似的框架,不過在以上這套訊息框架的基礎之上,做了 UI 的指令碼生成。在此之前筆者都是用 transform.Find 方式來獲取感興趣的 UI 控制元件的。比如 Button、Image 等等。而前輩的 UI 指令碼生成省去了這些工作量。實現方式也是比較容易理解。就是在一個 UI 的 Prefab 上,對於感興趣的控制元件掛上一個指令碼,比如 UIMark/UIBind。然後從 UI 的 Prefab 的 Root 開始進行深度優先搜尋。搜尋過程中記錄每個標記指令碼的路徑,之後根據路徑生成一行行的 transform.Find(路徑)就好了。而這個工具則是節省了製作 UI Prefab 過程中的體力勞動。是對工作流上的優化。QFramework 又收錄了一個工具。

支撐團隊協作

團隊協作的一個基礎就是將業務模組化。而業務很多時候是在完成大量的 UI 介面。在這裡簡單分享下筆者的做法。筆者首先會為每個 UI 介面都建立一個測試場景,只要執行 Unity 就可以看到 UI 介面的效果。這樣做的目的很簡單,就是方便快速修改,並且介面之間互相獨立,只要約定好誰來負責哪個模組,就不會造成版本控制衝突。 還有一個建議要做的就是,為每個 UI 介面都提供一個 Init 介面。一些 UI 介面要用的資料,筆者建議是從一開始通過初始化傳進去,而不是在 UI 裡面去訪問某個 Manager。這一點要做到需要花些功夫,不過好處就是 UI 作為一個黑盒,沒有上下文,可以傳入一些測試資料而不是真實資料就可以看效果並做一些測試了,當專案規模變大時,改一個 UI 介面或者查詢一個 bug 都會變得很容易。這樣的做法解決了多個問題,一石 N 鳥。

C# 進階

C# 真的是越用越覺得它的強大。QFramework 的進步是離不開 C# 語言的學習的。這裡筆者遇到了一個決定 QFramework 未來的語法特性,就是靜態 this 擴充套件。語法細節這裡不多說,大家自行百度。學習了這個語法之後,一些本來要靠繼承才能實現的 cocos2dx 風格的 API 全部可以用這個語法實現。簡直不要太好用!都後來的鏈式結構程式設計全都是以這個為基礎的。

QFramework 成為公司的指定框架

在筆者的堅持下,經過了團隊的 Code Review 之後,大家終於統一了使用 QFramework 作為公司的框架。從這時候開始 QFramework 開始飛速發展。

第一個專案

第一個專案三個人完成的,架構階段以筆者之前定的程式碼規範為基礎與團隊成員共同完成了專案的程式碼規範,隨後完成了專案結構目錄約定等等一系列約定,之後與專案的負責人根據專案需求確定了外掛的選擇,而框架自然就用 QFramework 了。除了以上這些還做了一件事,就是畫了一張不知道是什麼的圖。

上邊又有排期,又有分工,又有一些技術實現細節,還有各個模組的劃分。總之看著很亂,但是它的作用就是讓我們三個人很清晰地對專案的各個結構,以及近期的排期等資訊,專案的難點也一目瞭然。做好排期和分析後,就開始進行開發了,最終這個專案不管是時間還是品質上,都完成得很不錯。這就是充分(相比之前)做架構的好處。

競爭對手出現

在做第一個專案的時候,來了一位大牛,帶著一套 MMO 框架。框架好用的工具真的很多。

其中的 EventSystem(訊息系統)和 ResSystem(資源系統)是兩大亮點。EventSystem 的 EventId 是用泛型進行註冊的。把一個泛型轉換為 int 。這個解決了之前筆者註冊事件時需要把列舉強轉成 ushort 的問題,這樣的程式碼寫起來很不愉快,於是筆者把原來 MgrBehaviour 和 QMonoBehaviour 裡關於訊息註冊和轉發的程式碼殺掉,直接換成了 EventSystem 就 OK 了,QMonoBehaviour 和 MgrBehaviour 裡的程式碼變得非常精簡。而 ResSystem 使用非常簡單和強大。ResSystem 是在 AssetBundleManager 的功能基礎之上有抽象出來了 ResLoader。這樣做有什麼好處呢?

首先 AssetBundleManager 在哪裡載入了什麼資源和解除安裝了資源需要使用人腦進行記憶,專案體量很大時很容易由於忘記解除安裝資源而造成記憶體洩露。而 ResLoader 是一個物件,可以每個介面都申請一個 ResLoader 物件。所有在這個介面載入過的資源的資訊都會記錄到 ResLoader 裡,而解除安裝很簡單,只要在 OnDestroy 裡直接進行 ResLoader 的解除安裝就好了,非常方便。但是這時候筆者已經用慣了 AssetBundleManager 的打包方式,所以只收錄了 ResSystem 中除打包以外的程式碼。這裡簡單提一下,ResLoader 是用物件池實現物件的申請和回收的。而 ResSystem 裡的資源積累則是使用引用計數器決定資源的釋放的。在這裡 QFramework 收錄了 EventSystem、ResSystem、引用計數器、物件池,可以說收穫頗豐。

提拔帶人

做完第一個專案之後,被 Leader 提拔,開始帶人帶團隊。在框架和架構進行探索的時間少了很多。QFramework 在這之後邊也加了一些庫,比如 UniRx,ActionKit 等等。最終就是現在的 ActionKit、UI Kit、Res Kit 為核心的 QFramework 了。ActionKit 專注非同步邏輯和狀態機,可以很好地完成 GamePlay 需求和非同步需求。而 UI Kit 是 UI 的解決方案,裡邊還是包含著之前的基於模組的訊息框架。 Res Kit 則是解決資源管理方案。這裡不多說 Action Kit。在這裡筆者的經歷分享完了。

總結

  • 架構是“約定、規則、共識”
  • 框架具有約束性和支撐性
  • 好的架構直接就等於你要有一套好的規則,好的準則

  • Unity 好的規則
    1. 使用 C# 而不用 JavaScript
    2. 命名的時候要起一個比較有含義的名字
    3. 起資料夾的名字時儘量和 GameObject 對應起來
  • MVC 檔案結構:先按照業務功能劃分,再按照 MVC 來劃分
  • 程式碼是資產
  • 做完每一個專案都積累一些東西
  • QFramework
    * 工具集
    * FSM
    * Singleton&MonoSingleton
    * QEventSystem
    * MathUtils
    * 筆者知識積累工具
    * 業務支撐工具集(支撐性)
    * UI Kit
    * UIManager
    * 程式碼生成
    * MgrBehaviour、MonoBehaivour(約束性)
    * Res Kit
    * AudioManager
    * Action Kit
    * 建議
    1. 為每個 UI 介面都建立一個測試場景。
    2. 為每個 UI 介面都提供一個 Init 介面。
    3. UI 介面的 Init 介面傳資料,而不是在 UI 裡面去訪問某個 Manager Or Instance。

  • 在專案準備的架構活動
    1. 需求/業務整理、收集、分析
    2. 編碼規範、專案結構約定、資源命名規範、程式結構約定、模組/MVC 劃分、成員分工
    3. 外掛購買、造輪子、框架選型

這裡可以得出框架與架構關係的結論,框架可以解決一部分架構問題,使用框架 本身就是一種“約定、規則、共識”。

直到文章的結尾,QFramework 還是沒有收集到關於框架的約束性相關的內容。唯一能扯上點關係的就是基於模組的訊息框架這塊了。其實像 StrangeIOC、uFrame、PureMVC 等框架可以更容易去講解約束性相關的內容。Any way 這次就講到這裡吧,我們以後見。

推薦資料

  1. 《UNITE -Unity專案架構設計與開發管理》
  2. 《架構漫談》
  3. 《Unity3D手遊開發實踐》
  4. 《10年感觸:架構是什麼?——消滅架構!》
  5. 《涼鞋的筆記》

轉載請註明地址:涼鞋的筆記:liangxiegame.com

更多內容

  • QFramework 地址:https://github.com/liangxiegame/QFramework
  • QQ 交流群:623597263
  • 涼鞋的筆記:liangxiegame.com