本次分享總結,起源於騰訊桌球項目,可是不只僅限於項目自己。雖然基於Unity3D,不少東西一樣適用於Cocos。本文從如下10大點進行闡述:css
- 架構設計
- 原生插件/平臺交互
- 版本與補丁
- 用腳本,仍是不用?這是一個問題
- 資源管理
- 性能優化
- 異常與Crash
- 適配與兼容
- 調試及開發工具
- 項目運營
1.架構設計html
好的架構利用大規模項目的多人團隊開發和代碼管理,也利用查找錯誤和後期維護。java
- 框架的選擇:須要根據團隊、項目來進行選擇,沒有最好的框架,只有最合適的框架。
- 框架的使用:統一的框架能規範你們的行爲,互相之間能夠比較平滑切換,可維護性大大提高。除此以外,還能代碼解耦。例如StrangeIOC是一個超輕量級和高度可擴展的控制反轉(IoC)框架,專門爲C#和Unity編寫。已知公司內部使用StrangeIOC框架的遊戲有:騰訊桌球、歡樂麻將、植物大戰殭屍Online。<https://github.com/strangeioc/strangeioc>
依賴注入(Dependency Injection,簡稱DI),是一個重要的面向對象編程的法則來削減計算機程序的耦合問題。依賴注入還有一個名字叫作控制反轉(Inversion of Control,英文縮寫爲IoC)。依賴注入是這樣一個過程:因爲某客戶類只依賴於服務類的一個接口,而不依賴於具體服務類,因此客戶類只定義一個注入點。在程序運行過程當中,客戶類不直接實例化具體服務類實例,而是客戶類的運行上下文環境或專門組件負責實例化服務類,而後將其注入到客戶類中,保證客戶類的正常運行。即對象在被建立的時候,由一個運行上下文環境或專門組件將其所依賴的服務類對象的引用傳遞給它。也能夠說,依賴被注入到對象中。因此,控制反轉是,關於一個對象如何獲取他所依賴的對象的引用,這個責任的反轉。android
StrangeIOC採用MVCS(數據模型 Model,展現視圖 View,邏輯控制 Controller,服務Service)結構,經過消息/信號進行交互和通訊。整個MVCS框架跟flash的robotlegs基本一致,(忽略語言不同)詳細的參考<http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html>。ios
- 數據模型 Model:主要負責數據的存儲和基本數據處理
- 展現視圖 View:主要負責UI界面展現和動畫表現的處理
- 邏輯控制 Controller:主要負責業務邏輯處理,
- 服務Service:主要負責獨立的網絡收發請求等的一些功能。
- 消息/信號:經過消息/信號去解耦Model、View、Controller、Service這四種模塊,他們之間經過消息/信號進行交互。
- 綁定器Binder:負責綁定消息處理、接口與實例對象、View與Mediator的對應關係。
- MVCS Context:能夠理解爲MVC各個模塊存在的上下文,負責MVC綁定和實例的建立工做。
騰訊桌球客戶端項目框架git
-
代碼目錄的組織: 通常客戶端用得比較多的MVC框架,怎麼劃分目錄?
- 先按業務功能劃分,再按照 MVC 來劃分。"蛋糕心語"就是使用的這種方式。
- 先按 MVC 劃分,再按照業務功能劃分。"D9"、"寶寶鬥場"、"魔法花園"、"騰訊桌球"、"歡樂麻將"使用的這種方式。
根據使用習慣,能夠自行選擇。我的推薦"先按業務功能劃分,再按照 MVC 來劃分",使得模塊更聚焦(高內聚),第二種方式用多了發現隨着項目的運營模塊增多,沒有第一種那麼好維護。github
- Unity項目目錄的組織:結合Unity規定的一些特殊的用途的文件夾,咱們建議Unity項目文件夾組織方式以下。
其中,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。windows
-
公司組件
- 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)
- 將源碼編譯爲託管模塊;
- 將託管模塊組合爲程序集;
- 加載公共語言運行時CLR;
- 執行程序集代碼。
注:CLR(公共語言運行時,Common Language Runtime)和Java虛擬機同樣也是一個運行時環境,它負責資源管理(內存分配和垃圾收集),並保證應用和底層操做系統之間必要的分離。
爲了提升平臺的可靠性,以及爲了達到面向事務的電子商務應用所要求的穩定性級別,CLR還要負責其餘一些任務,好比監視程序的運行。按照.NET的說法,在CLR監視之下運行的程序屬於"託管"(managed)代碼,而不在CLR之下、直接在裸機上運行的應用或者組件屬於"非託管"(unmanaged)的代碼。
這幾個過程我總結爲下圖:
圖 .NET上的程序運行
回調函數是託管代碼C#中的定義的函數,對回調函數的調用,實現從非託管C/C++代碼中調用託管C#代碼。那麼C/C++是如何調用C#的呢?大體分爲2步,能夠用下圖表示:
- 將回調函數指針註冊到非託管C/C++代碼中(C#中回調函數指委託delegate)
- 調用註冊過的託管C#函數指針
相比較託管調用非託管,回調函數方式稍微複雜一些。回調函數很是適合重複執行的任務、異步調用等狀況下使用。
由上面的介紹能夠知道CLR提供了C#程序運行的環境,與非託管代碼的C/C++交互調用也由它來完成。CLR提供兩種用於與非託管C/C++代碼進行交互的機制:
- 平臺調用(Platform Invoke,簡稱PInvoke或者P/Invoke),它使託管代碼可以調用從非託管DLL中導出的函數。
- COM 互操做,它使託管代碼可以經過接口與組件對象模型 (COM) 對象交互。考慮跨平臺性,Unity3D不使用這種方式。
平臺調用依賴於元數據在運行時查找導出的函數並封送(Marshal)其參數。 下圖顯示了這一過程。
注意: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-https://en.wikipedia.org/wiki/Java_Native_Interface,這裏不深刻介紹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通訊成爲了可能,以下圖所示:
注: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函數。
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),你能夠像下面這樣作:
- 對於類,使用jni_env->FindClass得到類對象的引用
- 對於字段,使用jni_env->GetFieldId得到字段ID
- 使用對應的方法(例如jni_env->GetIntField)獲取字段的值
相似地,要調用一個方法,你step1.得得到一個類對象的引用obj,step2.是方法methodID。這些ID一般是指向運行時內部數據結構。查找到它們須要些字符串比較,但一旦你實際去執行它們得到字段或者作方法調用是很是快的。step3.調用jni_env->CallVoidMethodV(obj, methodID, args)。
從上面的示例代碼,咱們能夠看出使用原始的JNI方式去與Android(Java)插件交互是多的繁瑣,要本身作太多的事情,而且爲了性能須要本身考慮緩存查詢到的方法ID,字段ID等等。幸運的是,Unity3D已經爲咱們封裝好了這些,而且考慮了性能優化。Unity3D主要提供了一下2個級別的封裝來幫助高效編寫代碼:
注:Unity3D中對應的C/C++橋接器包含在libunity.so中。
- Level 1:AndroidJNI、AndroidJNIHelper,原始的封裝至關於咱們上面本身編寫的C# Wrapper。AndroidJNIHelper 和AndroidJNI自動完成了不少任務(指找到類定義,構造方法等),而且使用緩存使調用java速度更快。AndroidJavaObject和AndroidJavaClass基於AndroidJNIHelper 和AndroidJNI建立,但在處理自動完成部分也有不少本身的邏輯,這些類也有靜態的版本,用來訪問java類的靜態成員。更詳細接口參考幫助文檔:http://docs.unity3d.com/ScriptReference/AndroidJNI.html,http://docs.unity3d.com/ScriptReference/AndroidJNIHelper.html
- 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++徹底同樣:
除此以外,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便可實現動態更新資源的功能。
手遊在實現這塊時須要注意的幾點:
- 遊戲發佈出必定要提供遊戲內更新的途徑。即便是刪掉測試,保不許這期間須要進行資源或者BUG修復更新。不少玩家並不知道如何更新,並且Android手機應用分發平臺多樣,分發平臺自己也不會跟官方同步更新(特別是小的分發平臺)。
- 更新功能要提供強制更新、非強制更新配置化選項,並指定哪些版本能夠不強更,哪些版本必須強更。
-
當遊戲提供非強制更新功能以後,現網必定會存在多個版本。若是須要針對不一樣版本作不一樣的更新,例如配置文件A針對1.0.0.1修改了一項,針對1.0.0.2修改了另外一項,2個版本須要分別更新對應的修改,須要本身實現更新策略IIPS不提供這個功能。當須要複雜的更新策略,推薦本身編寫更新服務器和客戶端邏輯,不使用iips組件(其實本身實現也很簡單)。
沒有運營經驗的人會選擇二進制,認爲二進制安全、更小,這對端遊/手遊外網只存在一個版本的遊戲適合,對通常不強升版本的手遊並不適合,反而會對更新和維護帶來很大的麻煩。
-
配置使用XML或者JSON等文本格式,更利於多版本的兼容和更新。最開始騰訊桌球客戶端使用的二進制格式(由excel轉換而來),可是隨着運營配置格式須要增長字段,這樣老版本程序就解析不了新的二進制數據,給兼容和更新帶來了很大的麻煩。這樣就要求上面提到的針對不一樣步作不一樣更新,又或者配置一開始就預留好足夠的擴展項,其實無論怎麼預留擴展也很難跟上需求的變化,並且一開始會把配置表複雜化可是其實只有一張或者幾張纔會變動結構。
- iOS版本的送審版本須要鏈接特定的包含新內容的服務器,現網服務器還不包含新內容。送審經過以後,上架遊戲現網服務器會進行更新,iOS版本須要鏈接現網服務器而非送審服務器,可是這期間又不能修改客戶度,這個切換須要經過服務器下發開關進行控制。例如經過指定送審的iOS遊戲版本號,客戶端判斷本地版本號是否爲送審版本,若是是鏈接送審服務器,不然鏈接現網服務器。
4.用腳本,仍是不用?這是一個問題
方便更新,減小Crash(特別是使用C++的cocos引擎)
經過上面一節【版本與補丁】知道要實現代碼更新是很是困難的,正式這個緣由客戶端開發的壓力是比較大的,若是出現了比較嚴重的BUG必須發強制更新版本,使用腳本能夠解決這個問題。
因爲Unity3D手遊更新成本比較大,並且目前騰訊桌球要求不能強制更新,這致使新版本的活動覆蓋率提高比較慢、出現問題以後難以修復。針對這個狀況,考慮引入lua進行活動開發,後續發佈活動及修復bug只須要發佈lua資源,進行資源更新便可,大大下降了發佈和修復問題的成本。
可選方案還有使用Html5進行活動開發,目前遊戲中已經預埋了Html5活動入口,而且已經用來發過"玩家調查"、"騰訊棋牌宣傳"等。可是與lua對比,不能作到與Unity3D的深度融合,體驗不如使用lua,例如不能操做遊戲中的ui、不能完成複雜界面的製做、不能複用已有的功能、玩家付費充值跟已有的也會有差別
遊戲腳本之王——Lua
在公司內部魔方比較喜歡用lua,火隱忍者(手遊)unity+ulua,全民水滸cocos2d-x+lua等等都有使用lua進行開發。咱們可使用公司內部的xlua組件,也可使用ulua<http://ulua.org/>、UniLua<https://github.com/xebecnan/UniLua>等等。
5.資源管理
5.1資源管理器
業務不要直接使用引擎或者系統原生接口,而是封裝一個資源管理器負責:資源加載、卸載
兼容Resource.Load與AssetBundle資源互相變動需求,開發期間使用Resource.Load方式而沒必要打AB包效率更高
加載資源時,無論是同步加載仍是異步加載,最好是使用異步編碼方式(回調函數或者消息通知機制)。若是哪一天資源由本地加載改成從服務器按需加載,而遊戲中的邏輯都是同步方式編碼的,改起來將很是痛苦。其實異步編碼方式很簡單,不比同步方式複雜。
5.2資源類型
- 圖片/紋理(對性能、包體影響最大因素)
-
音頻
- 背景音樂,騰訊桌球使用.ogg/.mp3
- 音效,騰訊桌球使用.wav
-
數據
- 文本
- 二進制
- 動畫/特效
5.3圖片-文件格式與紋理格式
經常使用的圖像文件格式有BMP,TGA,JPG,GIF,PNG等;
經常使用的紋理格式有R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等。
文件格式是圖像爲了存儲信息而使用的對信息的特殊編碼方式,它存儲在磁盤中,或者內存中,可是並不能被GPU所識別,由於以向量計算見長的GPU對於這些複雜的計算無能爲力。這些文件格式當被遊戲讀入後,仍是須要通過CPU解壓成R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等像素格式,再傳送到GPU端進行使用。
紋理格式是能被GPU所識別的像素格式,能被快速尋址並採樣。舉個例子,DDS文件是遊戲開發中經常使用的文件格式,它內部能夠包含A4R4G4B4的紋理格式,也能夠包含A8R8G8B8的紋理格式,甚至能夠包含DXT1的紋理格式。在這裏DDS文件有點容器的意味。OpenGL ES 2.0支持以上提到的R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8,A8R8G8B8等紋理格式,其中 R5G6B5,A4R4G4B4,A1R5G5B5每一個像素佔用2個字節(BYTE),R8G8B8每一個像素佔用3個字節,A8R8G8B8每一個像素佔用 4個字節。
基於OpenGL ES的壓縮紋理有常見的以下幾種實現:
1)ETC1(Ericsson texture compression),ETC1格式是OpenGL ES圖形標準的一部分,而且被全部的Android設備所支持。
2)PVRTC (PowerVR texture compression),支持的GPU爲Imagination Technologies的PowerVR SGX系列。
3)ATITC (ATI texture compression),支持的GPU爲Qualcomm的Adreno系列。
4)S3TC (S3 texture compression),也被稱爲DXTC,在PC上普遍被使用,可是在移動設備上仍是屬於新鮮事物。支持的GPU爲NVIDIA Tegra系列。
5.4資源工具
有了規範就能夠作工具檢查,從源頭到打包
- 資源導入檢查
- 資源打包檢查
6.性能優化
掉幀主要針對GPU和CPU作分析;內存佔用大主要針對美術資源,音效,配置表,緩存等分析;卡頓也須要對GPU和CPU峯值分析,另外IO或者GC也易致使。
6.1工欲善其事,必先利其器
- Unity Profiler
- XCode instruments
- Qualcomm Adreno Profiler
- NVIDIA PerfHUD ES Tegra
6.2CPU:最佳原則減小計算
- 複用,UIScrollView Item複用,避免頻繁建立銷燬對象
- 緩存,例如Transform
-
運算裁剪,例如碰撞檢測裁剪
- 粗略碰撞檢測(劃分空間——二分/四叉樹/八叉樹/網格等,下降碰撞檢測的數量)
- 精確碰撞檢測(檢查候選碰撞結果,進而肯定對象是否真實發生碰撞)
- 休眠機制:避免模擬靜止的球
- 邏輯幀與渲染幀分離
- 分幀處理
- 異步/多線程處理
6.3GPU:最佳原則減小渲染
- 紋理壓縮
- 批處理減小DrawCall(unity-Static Batching和Dynamic Batching,cocos SpriteBatchNode)
- 減小無效/沒必要要繪製:屏幕外的裁剪,Flash髒矩陣算法,
- LOD/特效分檔
- NGUI動靜分離(UIPanel.LateUpdate的消耗)
- 控制角色骨骼數、模型面數/頂點數
- 降幀,並不是全部場景都須要60幀(騰訊桌球遊戲場景60幀,其餘場景30幀;每天酷跑,在開始遊戲前,FPS被限制爲30,遊戲開始以後FPS才爲60。每天飛車的FPS爲30,可是當用戶一段時間不點擊界面後,FPS自動降)
6.4內存:最佳原則減小內存分配/碎片、及時釋放
- 紋理壓縮-Android ETC一、iOS PVRTC 4bpp、windows DXT5
- 對象池-PoolManager
- 合併空閒圖集
- UI九宮格
- 刪除不用的腳本(也會佔用內存)
6.5IO:最佳原則減小/異步io
- 資源異步/多線程加載
- 預加載
- 文件壓縮
- 合理規劃資源合併打包,並不是texturepacker打包成大圖集必定好,會增長文件io時間
6.6網絡:其實也是IO的一種
使用單線程——共用UI線程,經過事件/UI循環驅動;仍是多線程——單獨的網絡線程?
-
單線程:由遊戲循環(事件)驅動,單線程模式比使用多線程模式開發、維護簡單不少,可是性能比多線程要差一些,因此在網絡IO的時候,須要注意別阻塞到遊戲循環。說明,若是網絡IO不復雜的狀況下,推薦使用該模式。
- 在UI線程中,別調用可能阻塞的網絡函數,優先考慮非阻塞IO
- 這是網絡開發者常常犯的錯誤之一。好比:作一個簡單如 gethostbyname() 的調用,這個操做在小範圍中不會存在任何問題,可是在有些狀況中現實世界的玩家卻會所以阻塞數分鐘之久!若是你在 GUI 線程中調用這樣一個函數,對於用戶來講,在函數阻塞時,GUI 一直都處於 frozen 或者 hanged 狀態,這從用戶體驗的角度是絕對不容許的。
-
多線程:單獨的網絡線程,使用獨立的網絡線程有一個很是明顯的好處,主線程能夠將髒活、累活交給網絡線程作使得UI更流暢,例如消息的編解碼、加解密工做,這些都是很是耗時的。可是使用多線程,給開發和維護帶來必定成本,而且若是沒有必定的經驗寫出來的網絡庫不那麼穩定,容易出錯,甚至致使遊戲崩潰。下面是幾點注意事項:
- 千萬千萬別在網絡線程中,回調主線程(UI線程)的回調函數。而是網絡線程將數據準備好,讓主線程主動去取,亦或者說網絡線程將網絡數據做爲一個事件驅動主線程去取。當年我在用Cocos2d-x + Lua作魔法花園的手機demo時,就採用的多線程模式,最初在網絡線程直接調用主線程回調函數,常常會致使莫名其妙的Crash。由於網絡線程中沒有渲染所必須的opengl上下文,會致使渲染出問題而Crash。
6.6包大小
- 使用壓縮格式的紋理/音頻
- 儘可能不要使用System.Xml而使用較小的Mono.Xml
- 啓用Stripping來減少庫的大小
- Unity strip level(strip by byte code)
- Unity3D輸出APK,取消X86架構
- iOS Xcode strip開啓
6.7耗電
下面影響耗電的幾個因素和影響度摘自公司內部的一篇文章。
7.異常與Crash
7.1防護式編程
-
非法的輸入中保護你的程序
- 檢查每一輸入參數
- 檢查來自外部的數據/資源
- 斷言
- 錯誤處理
- 隔欄
防不勝防,無論如何防護總有失手的時候,這就須要異常捕獲和上報。
7.2異常捕獲
異常捕獲已經有不少第三組件可供接入,這裏不介紹組件的而接入,而是簡單談一下異常捕獲的原理。
因爲不少錯誤並非發生在開發工做者調試階段,而是在用戶或測試工做者使用階段;這就須要相關代碼維護工做者對於程序異常捕獲收集現場信息。異常與Crash的監控和上報,這裏不介紹Bugly的使用,按照apollo或者msdk的文檔接入便可,沒有太多能夠說的。這裏主要透過Bugly介紹手遊的幾類異常的捕獲和分析:
- Unity3D C#層異常捕獲:比較簡單使用Application.RegisterLogCallback/Application.RegisterLogCallbackThreaded(在一個新的線程中調用委託)註冊回調函數。特別注意:保證項目中只有一個Application.RegisterLogCallback註冊回調,不然後面註冊的會覆蓋前面註冊的回調!回調函數中stackTrace參數包異常調用棧。
public void HandleLog(string logString, string stackTrace, LogType type) { if (logString == null || logString.StartsWith(cLogPrefix)) { return; }
ELogLevel level = ELogLevel.Verbose; switch (type) {
case LogType.Exception: level = ELogLevel.Error; break; default: return; }
if (stackTrace != null) { Print(level, ELogTag.UnityLog, logString + "\n" + stackTrace); } else { Print(level, ELogTag.UnityLog, logString); } } |
- Android Java層異常捕獲
try…catch顯式的捕獲異常通常是不引發遊戲Crash的,它又稱爲編譯時異常,即在編譯階段被處理的異常。編譯器會強制程序處理全部的Checked異常,由於Java認爲這類異常都是能夠被處理(修復)的。若是沒有try…catch這個異常,則編譯出錯,錯誤提示相似於"Unhandled exception type xxxxx"。
UnChecked異常又稱爲運行時異常,因爲沒有相應的try…catch處理該異常對象,因此Java運行環境將會終止,程序將退出,也就是咱們所說的Crash。那爲何不會加在try…catch呢?
- 沒法將全部的代碼都加上try…catch
- UnChecked異常一般都是較爲嚴重的異常,或者說已經破壞了運行環境的。好比內存地址,即便咱們try…catch住了,也不能明確知道如何處理該異常,才能保證程序接下來的運行是正確的。
Uncaught異常會致使應用程序崩潰。那麼當崩潰了,咱們是否能夠作些什麼呢,就像Application.RegisterLogCallback註冊回調打印日誌、上報服務器、彈窗提示用戶?Java提供了一個接口給咱們,能夠完成這些,這就是UncaughtExceptionHandler,該接口含有一個純虛函數:
public abstract void uncaughtException (Thread thread, Throwableex) |
Uncaught異常發生時會終止線程,此時,系統便會通知UncaughtExceptionHandler,告訴它被終止的線程以及對應的異常,而後便會調用uncaughtException函數。若是該handler沒有被顯式設置,則會調用對應線程組的默認handler。若是咱們要捕獲該異常,必須實現咱們本身的handler,並經過如下函數進行設置:
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) |
特別注意:屢次調用setDefaultUncaughtExceptionHandler設置handler,後面註冊的會覆蓋前面註冊的,以最後一次爲準。實現自定義的handler,只須要繼承UncaughtExceptionHandler該接口,並實現uncaughtException方法便可。
static class MyCrashHandler implements UncaughtExceptionHandler{ @Override public void uncaughtException(Thread thread, final Throwable throwable) { // Deal this exception } } |
在任何線程中,均可以經過setDefaultUncaughtExceptionHandler來設置handler,但在Android應用程序中,全局的Application和Activity、Service都同屬於UI主線程,線程名稱默認爲"main"。因此,在Application中應該爲UI主線程添加UncaughtExceptionHandler,這樣整個程序中的Activity、Service中出現的UncaughtException事件均可以被處理。
捕獲Exception以後,咱們還須要知道崩潰堆棧的信息,這樣有助於咱們分析崩潰的緣由,查找代碼的Bug。異常對象的printStackTrace方法用於打印異常的堆棧信息,根據printStackTrace方法的輸出結果,咱們能夠找到異常的源頭,並跟蹤到異常一路觸發的過程。
public static String getStackTraceInfo(final Throwable throwable) { String trace = ""; try { Writer writer = new StringWriter(); PrintWriter pw = new PrintWriter(writer); throwable.printStackTrace(pw); trace = writer.toString(); pw.close(); } catch (Exception e) { return ""; } return trace; } |
-
Android Native Crash:前面咱們知道能夠編寫和使用C/C++原生插件,除非C++使用try...catch捕獲異常,不然通常會直接crash,經過捕獲信號進行處理。
- iOS 異常捕獲:
跟Android、Unity相似,iOS也提供NSSetUncaughtExceptionHandler 來作異常處理。
#import "CatchCrash.h"
@implementation CatchCrash
void uncaughtExceptionHandler(NSException *exception) { // 異常的堆棧信息 NSArray *stackArray = [exception callStackSymbols]; // 出現異常的緣由 NSString *reason = [exception reason]; // 異常名稱 NSString *name = [exception name]; NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray]; NSLog(@"%@", exceptionInfo);
NSMutableArray *tmpArr = [NSMutableArray arrayWithArray:stackArray]; [tmpArr insertObject:reason atIndex:0];
[exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/error.log",NSHomeDirectory()] atomically:YES encoding:NSUTF8StringEncoding error:nil]; }
@end |
可是內存訪問錯誤、重複釋放等錯誤引發崩潰就無能爲力了,由於這種錯誤它拋出的是信號,因此還必需要專門作信號處理。
- windows crash:一樣windows提供SetUnhandledExceptionFilter函數,設置最高一級的異常處理函數,當程序出現任何未處理的異常,都會觸發你設置的函數裏,而後在異常處理函數中獲取程序異常時的調用堆棧、內存信息、線程信息等。
8.適配與兼容
8.1UI適配
- 錨點(UIAnchor、UIWidgetAnchor屬性)
- NGUI UIRoot統一設置縮放比例
- UIStretch
8.2兼容
- shader兼容:例如if語句有的機型支持很差,Google nexus 6在shader中使用了if就會crash
- 字體兼容:android複雜的環境,有的手機廠商和rom會對字體進行優化,去掉android默認字體,若是不打包字體會不現實中文字
9.調試及開發工具
9.1日誌及跟蹤
事實證實,打印日誌(printf調試法)是很是有效的方法。一個好用的日誌調試,必備如下幾個功能:
- 日誌面板/控制檯,格式化輸出
- 冗長級別(verbosity level):ERROR、WARN、INFO、DEBUG
- 頻道(channel):按功能等進行模塊劃分,如網絡頻道只接收/顯示網絡模塊的消息,頻道建議使用枚舉進行命名。
- 日誌同時會輸出到日誌文件
- 日誌上報
9.2調試用繪圖工具
調試繪圖用工具指開發及調試期間爲了可視化的繪圖用工具,如騰訊桌球開發調試時會使用VectrosityScripts可視化球桌的物理模型(實際碰撞線)幫助調試。這類工具能夠節省大量時間及快速定位問題。一般調試用繪圖工具包含:
- 支持繪製基本圖形,如直線、球體、點、座標軸、包圍盒等
- 支持自定義配置,如顏色、粒度(線的粗細/球體半徑/點的大小)等
9.3遊戲內置菜單/做弊工具
在開發調試期間提供遊戲進行中的一些配置選項及做弊工具,以方便調試和提升效率。例如騰訊桌球遊戲中提供:
- 遊戲內物理引擎參數調整菜單;
- 修改球杆瞄準線長度/反射線數量、修改簽到獎勵領取天數等做弊工具
注意遊戲內的全部開發調試用的工具,都須要經過編譯宏開關,保證發佈版本不會把工具代碼包含進去。
9.4Unity擴展
Untiy引擎提供了很是強大的編輯器擴展功能,基於Unity Editor能夠實現很是多的功能。公司內部、外部都有很是的開源擴展可用
公司外部,如GitHub上的:
…
公司內部:
TUT、BeautyUnity、UnityDependencyBy
10.項目運營
-
自動構建
-
版本號——主版本號.特性版本號.修正版本號.構建版本號
- [構建版本號]應用分發平臺升級判斷基準
-
自動構建
- Android
- iOS — XUPorter
-
公司內部接入SODA便可,建議搭建本身的構建機,開發期間每日N Build排隊會死人的,另外也能夠搭建本身的搭建構建平臺
-
統計上報
-
Tlog上報
- 玩家轉化關鍵步驟統計(重要)
- Ping統計上報
- 遊戲業務的統計上報(例如桌球球局相關的統計上報)
- 燈塔自定義上報
-
-
運營模板
- 配置化
- 服務器動態下發
- CDN拉取圖片並緩存
上線前的checklist
項目 |
要點 |
說明 |
指標 |
燈塔上報 |
1. 燈塔自帶統計信息 |
燈塔裏面包含不少統計數據,須要檢查是否ok |
1. 版本/渠道分佈 |
信鴿推送 |
可以針對單個玩家,全部玩家推送消息 |
||
米大師支付 |
正常支付 |
||
安全組件 |
1. TSS組件接入 |
根據安全中心提供的文檔完成全部項 |
接入安全組件,並經過安全中心的驗收 |
穩定性 |
crash率 |
用戶crash率:發生CRASH的用戶數/使用用戶數 |
低於3% |
弱網絡 |
斷線重連考慮,緩存消息,重發機制等等 |
客戶端的核心場景必須有斷線重連機制,並在有網絡抖動、延時、丟包的網絡場景下,客戶端需達到如下要求: |
|
兼容性 |
經過適配測試 |
||
遊戲更新 |
1. 整包更新 |
特別說明:iOS送審版本支持連特定環境,與正式環境區別開,須要經過服務器開關控制 |
|
性能 |
內存、CPU、幀率、流量、安裝包大小 |
【內存佔用要求】 |
做者:吳秦
出處:http://www.cnblogs.com/skynet/
本文基於署名 2.5 中國大陸許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名吳秦(包含連接).