1. 程式人生 > >unity 專案實踐經驗 和 架構體系

unity 專案實踐經驗 和 架構體系

GameRes遊資網授權釋出 文 / 吳秦(Tyler)

  本次分享總結,起源於騰訊桌球專案,但是不僅僅限於專案本身。雖然基於Unity3D,很多東西同樣適用於Cocos。本文從以下10大點進行闡述:

  1.架構設計

  2.原生外掛/平臺互動

  3.版本與補丁

  4.用指令碼,還是不用?這是一個問題

  5.資源管理

  6.效能優化

  7.異常與Crash

  8.適配與相容

  9.除錯及開發工具

  10.專案運營

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


 1、架構設計

  好的架構利用大規模專案的多人團隊開發和程式碼管理,也利用查詢錯誤和後期維護。


  • 框架的選擇:需要根據團隊、專案來進行選擇,沒有最好的框架,只有最合適的框架。
  • 框架的使用:統一的框架能規範大家的行為,互相之間可以比較平滑切換,可維護性大大提升。除此之外,還能程式碼解耦。例如StrangeIOC是一個超輕量級和高度可擴充套件的控制反轉(IoC)框架,專門為C#和Unity編寫。已知公司內部使用StrangeIOC框架的遊戲有:騰訊桌球、歡樂麻將、植物大戰殭屍Online。

  依賴注入(Dependency Injection,簡稱DI),是一個重要的面向物件程式設計的法則來削減計算機程式的耦合問題。依賴注入還有一個名字叫做控制反轉(Inversion of Control,英文縮寫為IoC)。依賴注入是這樣一個過程:由於某客戶類只依賴於服務類的一個介面,而不依賴於具體服務類,所以客戶類只定義一個注入點。在程式執行過程中,客戶類不直接例項化具體服務類例項,而是客戶類的執行上下文環境或專門元件負責例項化服務類,然後將其注入到客戶類中,保證客戶類的正常執行。即物件在被建立的時候,由一個執行上下文環境或專門元件將其所依賴的服務類物件的引用傳遞給它。也可以說,依賴被注入到物件中。所以,控制反轉是,關於一個物件如何獲取他所依賴的物件的引用,這個責任的反轉。


Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  StrangeIOC採用MVCS(資料模型?Model,展示檢視?View,邏輯控制?Controller,服務Service)結構,通過訊息/訊號進行互動和通訊。整個MVCS框架跟flash的robotlegs基本一致,(忽略語言不一樣)詳細的參考<http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html>

  • 資料模型 Model:主要負責資料的儲存和基本資料處理
  • 展示檢視 View:主要負責UI介面展示和動畫表現的處理
  • 邏輯控制 Controller:主要負責業務邏輯處理,
  • 服務Service:主要負責獨立的網路收發請求等的一些功能。
  • 訊息/訊號:通過訊息/訊號去解耦Model、View、Controller、Service這四種模組,他們之間通過訊息/訊號進行互動。
  • 繫結器Binder:負責繫結訊息處理、介面與例項物件、View與Mediator的對應關係。
  • MVCS Context:可以理解為MVC各個模組存在的上下文,負責MVC繫結和例項的建立工作。

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...

騰訊桌球客戶端專案框架


  • 程式碼目錄的組織:一般客戶端用得比較多的MVC框架,怎麼劃分目錄?
  • 先按業務功能劃分,再按照?MVC?來劃分。"蛋糕心語"就是使用的這種方式。
  • 先按MVC劃分,再按照業務功能劃分。"D9"、"寶寶鬥場"、"魔法花園"、"騰訊桌球"、"歡樂麻將"使用的這種方式。

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


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

  Unity專案目錄的組織:結合Unity規定的一些特殊的用途的資料夾,我們建議Unity專案資料夾組織方式如下。

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  其中,Plugins支援Plugins/{Platform}這樣的命名規範:

  • Plugins/x86
  • Plugins/x86_64
  • Plugins/Android
  • Plugins/iOS

  如果存在Plugins/{Platform},則載入Plugins/{Platform}目錄下的檔案,否則載入Plugins目錄下的,也就是說,如果存在{Platform}目錄,Plugins根目錄下的DLL是不會載入的。

  另外,資源組織採用分資料夾儲存"成品資源"及"原料資源"的方式處理:防止無關資源參與打包,RawResource即原始資源,Resource即成品資源。當然並不限於RawResource這種形式,其他Unity規定的特殊資料夾都可以這樣,例如Raw Standard Assets。

  公司元件

  • msdk(sns、支付midas、推送燈塔、監控Bugly)
  • apollo
  • apollo voice
  • xlua

  目前我們的騰訊桌球、四國軍棋都接入了apollo,但是如果伺服器不採用apollo框架,不建議客戶端接apollo,而是直接接msdk減少二次封裝資訊的丟失和帶來的錯誤,方便以後升級維護,並且減少匯入無用的程式碼。

  第三方外掛選型

  • NGUI
  • DoTween
  • GIF
  • GAF
  • VectrosityScripts
  • PoolManager
  • Mad Level Manger

2、原生外掛/平臺互動

  雖然大多時候使用Unity3D進行遊戲開發時,只需要使用C#進行邏輯編寫。但有時候不可避免的需要使用和編寫原生外掛,例如一些第三方外掛只提供C/C++原生外掛、複用已有的C/C++模組等。有一些功能是Unity3D實現不了,必須要呼叫Android/iOS原生介面,比如獲取手機的硬體資訊(UnityEngine.SystemInfo沒有提供的部分)、呼叫系統的原生彈窗、手機震動等等

 2.1C/C++外掛

  編寫和使用原生外掛的幾個關鍵點:

  建立C/C++原生外掛

  • 匯出介面必須是C ABI-compatible函式
  • 函式呼叫約定

  在C#中標識C/C++的函式並呼叫

  • 標識 DLL 中的函式。至少指定函式的名稱和包含該函式的 DLL 的名稱。
  • 建立用於容納 DLL 函式的類。可以使用現有類,為每一非託管函式建立單獨的類,或者建立包含一組相關的非託管函式的一個類。
  • 在託管程式碼中建立原型。使用?DllImportAttribute?標識 DLL 和函式。?用?static?和?extern?修飾符標記方法。
  • 呼叫 DLL 函式。像處理其他任何託管方法一樣呼叫託管類上的方法。

  在C#中建立回撥函式,C/C++呼叫C#回撥函式

  • 建立託管回撥函式。
  • 建立一個委託,並將其作為引數傳遞給?C/C++函式。平臺呼叫會自動將委託轉換為常見的回撥格式。
  • 確保在回撥函式完成其工作之前,垃圾回收器不會回收委託。

  那麼C#與原生外掛之間是如何實現互相呼叫的呢?在弄清楚這個問題之前,我們先看下C#程式碼(.NET上的程式)的執行的過程:(更詳細一點的介紹可以參見我之前寫的部落格:http://www.cnblogs.com/skynet/archive/2010/05/17/1737028.html

  1.將原始碼編譯為託管模組;

  2.將託管模組組合為程式集;

  3.載入公共語言執行時CLR;

  4.執行程式集程式碼。

  注:CLR(公共語言執行時,Common Language Runtime)和Java虛擬機器一樣也是一個執行時環境,它負責資源管理(記憶體分配和垃圾收集),並保證應用和底層作業系統之間必要的分離。

  為了提高平臺的可靠性,以及為了達到面向事務的電子商務應用所要求的穩定性級別,CLR還要負責其他一些任務,比如監視程式的執行。按照.NET的說法,在CLR監視之下執行的程式屬於"託管"(managed)程式碼,而不在CLR之下、直接在裸機上執行的應用或者元件屬於"非託管"(unmanaged)的程式碼。

  這幾個過程我總結為下圖:

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...

圖 .NET上的程式執行


  回撥函式是託管程式碼C#中的定義的函式,對回撥函式的呼叫,實現從非託管C/C++程式碼中呼叫託管C#程式碼。那麼C/C++是如何呼叫C#的呢?大致分為2步,可以用下圖表示:

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  將回調函式指標註冊到非託管C/C++程式碼中(C#中回撥函式指委託delegate)

  呼叫註冊過的託管C#函式指標

  相比較託管呼叫非託管,回撥函式方式稍微複雜一些。回撥函式非常適合重複執行的任務、非同步呼叫等情況下使用。

  由上面的介紹可以知道CLR提供了C#程式執行的環境,與非託管程式碼的C/C++互動呼叫也由它來完成。CLR提供兩種用於與非託管C/C++程式碼進行互動的機制:

  • 平臺呼叫(Platform Invoke,簡稱PInvoke或者P/Invoke),它使託管程式碼能夠呼叫從非託管DLL中匯出的函式。
  • COM 互操作,它使託管程式碼能夠通過介面與元件物件模型 (COM) 物件互動。考慮跨平臺性,Unity3D不使用這種方式。

  平臺呼叫依賴於元資料在執行時查詢匯出的函式並封送(Marshal)其引數。下圖顯示了這一過程。

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  注意:1.除涉及回撥函式時以外,平臺呼叫方法呼叫從託管程式碼流向非託管程式碼,而絕不會以相反方向流動。雖然平臺呼叫的呼叫只能從託管程式碼流向非託管程式碼,但是資料仍然可以作為輸入引數或輸出引數在兩個方向流動。2.圖中DLL表示動態庫,Windows平臺指.dll檔案、Linux/Android指.so檔案、Mac OS X指.dylib/framework檔案、iOS中只能使用.a。後文都使用DLL代指,並且DLL使用C/C++編寫。

  當"平臺呼叫"呼叫非託管函式時,它將依次執行以下操作:

  • 查詢包含該函式的DLL。
  • 將該DLL載入到記憶體中。
  • 查詢函式在記憶體中的地址並將其引數推到堆疊上,以封送所需的資料(引數)。
  • 將控制權轉移給非託管函式。

  注意:只在第一次呼叫函式時,才會查詢和載入 DLL 並查詢函式在記憶體中的地址。iOS中使用的是.a已經靜態打包到最終執行檔案中。

 2.2Android外掛

  Java同樣提供了這樣一個擴充套件機制JNI(Java Native Interface),能夠與C/C++互相通訊。

  注:

  • JNI wiki這裡不深入介紹JNI,有興趣的可以自行去研究。如果你還不知道JNI也不用怕,就像Unity3D使用C/C++庫一樣,用起來還是比較簡單的,只需要知道這個東西即可。並且Unity3D對C/C++橋接器這塊做了封裝,提供AndroidJNI/AndroidJNIHelper/AndroidJavaObject/AndroidJavaClass/AndroidJavaProxy方便使用等,具體使用後面在介紹。JNI提供了若干的API實現了Java和其他語言的通訊(主要是C&C++)。從Java1.1開始,JNI標準成為java平臺的一部分,它允許Java程式碼和其他語言寫的程式碼進行互動,保證原生代碼能工作在任何Java?虛擬機器環境下。"
  • 作為知識擴充套件,提一下Android Java虛擬機器。Android的Java虛擬機器有2個,最開始是Dalvik,後面Google在Android 4.4系統新增一種應用執行模式ART。ART與Dalvik 之間的主要區別是其具有提前 (AOT) 編譯模式。 根據 AOT 概念,裝置安裝應用時,DEX 位元組程式碼轉換僅進行一次。 相比於 Dalvik,這樣可實現真正的優勢 ,因為 Dalvik 的即時 (JIT) 編譯方法需要在每次執行應用時都進行程式碼轉換。下文中用Java虛擬機器代指Dalvik/ART。

  C#/Java都可以和C/C++通訊,那麼通過編寫一個C/C++模組作為橋接,就使得C#與Java通訊成為了可能,如下圖所示:

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  注:C/C++橋接器本身跟Unity3D沒有直接關係,不屬於Android和Unity3D,圖中放在Unity3D中是為了代指libunity.so中實現的橋接器以表示真實的情況。

  通過JNI既可以用於Java程式碼呼叫C/C++程式碼,也可用於C/C++程式碼與Java(Dalvik/ART虛擬機器)的互動。JNI定義了2個關鍵概念/結構:JavaVM、JNIENV。JavaVM提供虛擬機器建立、銷燬等操作,Java中一個程序可以建立多個虛擬機器,但是Android一個程序只能有一個虛擬機器。JNIENV是執行緒相關的,對應的是JavaVM中的當前執行緒的JNI環境,只有附加(attach)到JavaVM的執行緒才有JNIENV指標,通過JNIEVN指標可以獲取JNI功能,否則不能夠呼叫JNI函式。

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  C/C++要訪問的Java程式碼,必須要能訪問到Java虛擬機器,獲取虛擬機器有2中方法:

  • 在載入動態連結庫的時候,JVM會呼叫JNI_OnLoad(JavaVM* jvm, void* reserved),第一個引數會傳入JavaVM指標。
  • 在C/C++中呼叫JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args)建立JavaVM指標

  所以,我們只需要在編寫C/C++橋接器so的時候定義JNI_OnLoad(JavaVM* jvm, void* reserved)方法即可,然後把JavaVM指標儲存起來作為上下文使用。

  獲取到JavaVM之後,還不能直接拿到JNI函式去獲取Java程式碼,必須通過執行緒關聯的JNIENV指標去獲取。所以,作為一個好的開發習慣在每次獲取一個執行緒的JNI相關功能時,先呼叫AttachCurrentThread();又或者每次通過JavaVM指標獲取當前的JNIENV:java_vm->GetEnv((void**)&jni_env,?version),一定是已經附加到JavaVM的執行緒。通過JNIENV可以獲取到Java的程式碼,例如你想在原生代碼中訪問一個物件的欄位(field),你可以像下面這樣做:

  1.對於類,使用jni_env->FindClass獲得類物件的引用

  2.對於欄位,使用jni_env->GetFieldId獲得欄位ID

  3.使用對應的方法(例如jni_env->GetIntField)獲取欄位的值

  類似地,要呼叫一個方法,你step1.得獲得一個類物件的引用obj,step2.是方法methodID。這些ID通常是指向執行時內部資料結構。查詢到它們需要些字串比較,但一旦你實際去執行它們獲得欄位或者做方法呼叫是非常快的。step3.呼叫jni_env->CallVoidMethodV(obj,methodID,args)。

  從上面的示例程式碼,我們可以看出使用原始的JNI方式去與Android(Java)外掛互動是多的繁瑣,要自己做太多的事情,並且為了效能需要自己考慮快取查詢到的方法ID,欄位ID等等。幸運的是,Unity3D已經為我們封裝好了這些,並且考慮了效能優化。Unity3D主要提供了一下2個級別的封裝來幫助高效編寫程式碼:

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  注:Unity3D中對應的C/C++橋接器包含在libunity.so中。

  • Level 1:AndroidJNI、AndroidJNIHelper,原始的封裝相當於我們上面自己編寫的C# Wrapper。AndroidJNIHelper和AndroidJNI自動完成了很多工(指找到類定義,構造方法等),並且使用快取使呼叫java速度更快。AndroidJavaObject和AndroidJavaClass基於AndroidJNIHelper和AndroidJNI建立,但在處理自動完成部分也有很多自己的邏輯,這些類也有靜態的版本,用來訪問java類的靜態成員。
  • Level 2:AndroidJavaObject、AndroidJavaClass、AndroidJavaProxy,這個3個類是基於Level1的封裝提供了更高層級的封裝使用起來更簡單,這個在第三部分詳細介紹。

 2.3iOS外掛

  iOS編寫外掛比Android要簡單很多,因為Objective-C也是 C-compatible的,完全相容標準C語言。這些就可以非常簡單的包一層 extern "c"{},用C語言封裝呼叫iOS功能,暴露給Unity3D呼叫。並且可以跟原生C/C++庫一樣編成.a外掛。C#與iOS(Objective-C)通訊的原理跟C/C++完全一樣:

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  除此之外,Unity iOS支援外掛自動整合方式。所有位於Asset/Plugings/iOS資料夾中字尾名為.m , .mm , .c , .cpp的檔案都將自動併入到已生成的Xcode專案中。然而,最終編進執行檔案中。字尾為.h的檔案不能被包含在Xcode的專案樹中,但他們將出現在目標檔案系統中,從而使.m/.mm/.c/.cpp檔案編譯。這樣編寫iOS外掛,除了需要對iOS Objective-C有一定了解之外,與C/C++外掛沒有差異,反而更簡單。

3、版本與補丁

  任何遊戲(端遊、手遊)都應該提供遊戲內更新的途徑。一般遊戲分為全量更新/整包更新、增量更新、資源更新。

 全量

  android遊戲內完整安裝包下載(ios跳轉到AppStore下載)

增量:主要指android省流量更新

  • 可以使用bsdiff生成patch包
  • 應用寶也提供增量更新sdk可供接入

 資源

  Unity3D通過使用AssetBundle即可實現動態更新資源的功能。

  手遊在實現這塊時需要注意的幾點:

  1.遊戲釋出出一定要提供遊戲內更新的途徑。即使是刪掉測試,保不準這期間需要進行資源或者BUG修復更新。很多玩家並不知道如何更新,而且Android手機應用分發平臺多樣,分發平臺本身也不會跟官方同步更新(特別是小的分發平臺)。

  2.更新功能要提供強制更新、非強制更新配置化選項,並指定哪些版本可以不強更,哪些版本必須強更。

  3.當遊戲提供非強制更新功能之後,現網一定會存在多個版本。如果需要針對不同版本做不同的更新,例如配置檔案A針對1.0.0.1修改了一項,針對1.0.0.2修改了另一項,2個版本需要分別更新對應的修改,需要自己實現更新策略IIPS不提供這個功能。當需要複雜的更新策略,推薦自己編寫更新伺服器和客戶端邏輯,不使用iips元件(其實自己實現也很簡單)。

Unity3D手遊開發實踐《騰訊桌球》客戶端開發經驗總結 ...


  沒有運營經驗的人會選擇二進位制,認為二進位制安全、更小,這對端遊/手遊外網只存在一個版本的遊戲適合,對一般不強升版本的手遊並不適合,反而會對更新和維護帶來很大的麻煩。

  4.配置使用XML或者JSON等文字格式,更利於多版本的相容和更新。最開始騰訊桌球客戶端使用的二進位制格式(由excel轉換而來),但是隨著運營配置格式需要增加欄位,這樣老版本程式就解析不了新的二進位制資料,給相容和更新帶來了很大的麻煩。這樣就要求上面提到的針對不同步做不同更新,又或者配置一開始就預留好足夠的擴充套件項,其實不管怎麼預留擴充套件也很難跟上需求的變化,而且一開始會把配置表複雜化但是其實只有一張或者幾張才會變更結構。

  5.iOS版本的送審版本需要連線特定的包含新內容的伺服器,現網伺服器還不包含新內容。送審通過之後,上架遊戲現網伺服器會進行更新,iOS版本需要連線現網伺服器而非送審伺服器,但是這期間又不能修改客戶度,這個切換需要通過伺服器下發開關進行控制。例如通過指定送審的iOS遊戲版本號,客戶端判斷本地版本號是否為送審版本,如果是連線送審伺服器,否則連線現網伺服器。

 4、用指令碼,還是不用?這是一個問題