涼鞋:我所理解的框架 【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

更多內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM