次分享總結,起源于騰訊桌球項目,但是不僅僅限于項目本身。雖然基于Unity3D,很多東西同樣適用于Cocos。本文從以下10大點進行闡述:
1.架構設計
2.原生插件/平臺交互
3.版本與補丁
4.用腳本,還是不用?這是一個問題
5.資源管理
6.性能優化
7.異常與Crash
8.適配與兼容
9.調試及開發工具
10.項目運營
1、架構設計
好的架構利用大規模項目的多人團隊開發和代碼管理,也利用查找錯誤和后期維護。
框架的選擇:需要根據團隊、項目來進行選擇,沒有最好的框架,只有最合適的框架。
框架的使用:統一的框架能規范大家的行為,互相之間可以比較平滑切換,可維護性大大提升。除此之外,還能代碼解耦。例如StrangeIOC是一個超輕量級和高度可擴展的控制反轉(IoC)框架,專門為C#和Unity編寫。已知公司內部使用StrangeIOC框架的游戲有:騰訊桌球、歡樂麻將、植物大戰僵尸Online。
依賴注入(Dependency Injection,簡稱DI),是一個重要的面向對象編程的法則來削減計算機程序的耦合問題。依賴注入還有一個名字叫做控制反轉(Inversion of Control,英文縮寫為IoC)。依賴注入是這樣一個過程:由于某客戶類只依賴于服務類的一個接口,而不依賴于具體服務類,所以客戶類只定義一個注入點。在程序運行過程中,客戶類不直接實例化具體服務類實例,而是客戶類的運行上下文環境或專門組件負責實例化服務類,然后將其注入到客戶類中,保證客戶類的正常運行。即對象在被創建的時候,由一個運行上下文環境或專門組件將其所依賴的服務類對象的引用傳遞給它。也可以說,依賴被注入到對象中。所以,控制反轉是,關于一個對象如何獲取他所依賴的對象的引用,這個責任的反轉。
StrangeIOC采用MVCS(數據模型?Model,展示視圖?View,邏輯控制?Controller,服務Service)結構,通過消息/信號進行交互和通信。整個MVCS框架跟flash的robotlegs基本一致。
數據模型 Model:主要負責數據的存儲和基本數據處理
展示視圖 View:主要負責UI界面展示和動畫表現的處理
邏輯控制 Controller:主要負責業務邏輯處理,
服務Service:主要負責獨立的網絡收發請求等的一些功能。
消息/信號:通過消息/信號去解耦Model、View、Controller、Service這四種模塊,他們之間通過消息/信號進行交互。
綁定器Binder:負責綁定消息處理、接口與實例對象、View與Mediator的對應關系。
MVCS Context:可以理解為MVC各個模塊存在的上下文,負責MVC綁定和實例的創建工作。
騰訊桌球客戶端項目框架
代碼目錄的組織:一般客戶端用得比較多的MVC框架,怎么劃分目錄?
先按業務功能劃分,再按照?MVC?來劃分。"蛋糕心語"就是使用的這種方式。
先按MVC劃分,再按照業務功能劃分。"D9"、"寶寶斗場"、"魔法花園"、"騰訊桌球"、"歡樂麻將"使用的這種方式。
根據使用習慣,可以自行選擇。個人推薦"先按業務功能劃分,再按照 MVC 來劃分",使得模塊更聚焦(高內聚),第二種方式用多了發現隨著項目的運營模塊增多,沒有第一種那么好維護。
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。
公司組件
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#與原生插件之間是如何實現互相調用的呢?
1.將源碼編譯為托管模塊;
2.將托管模塊組合為程序集;
3.加載公共語言運行時CLR;
4.執行程序集代碼。
注: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這里不深入介紹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),你可以像下面這樣做:
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中對應的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++完全一樣:
除此之外,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組件(其實自己實現也很簡單)。
沒有運營經驗的人會選擇二進制,認為二進制安全、更小,這對端游/手游外網只存在一個版本的游戲適合,對一般不強升版本的手游并不適合,反而會對更新和維護帶來很大的麻煩。
4.配置使用XML或者JSON等文本格式,更利于多版本的兼容和更新。最開始騰訊桌球客戶端使用的二進制格式(由excel轉換而來),但是隨著運營配置格式需要增加字段,這樣老版本程序就解析不了新的二進制數據,給兼容和更新帶來了很大的麻煩。這樣就要求上面提到的針對不同步做不同更新,又或者配置一開始就預留好足夠的擴展項,其實不管怎么預留擴展也很難跟上需求的變化,而且一開始會把配置表復雜化但是其實只有一張或者幾張才會變更結構。
5.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、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 ETC1、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.7包大小
使用壓縮格式的紋理/音頻
盡量不要使用System.Xml而使用較小的Mono.Xml
啟用Stripping來減小庫的大小
Unity strip level(strip by byte code)
Unity3D輸出APK,取消X86架構
iOS?Xcode?strip開啟
6.8耗電
下面影響耗電的幾個因素和影響度摘自公司內部的一篇文章。
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);
}
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上的:
UnityEditor-MiniExtension
Unity-Resource-Checker
UnityEditorHelper
MissingReferencesUnity
Unity3D-ExtendedEditor
…
公司內部:
TUT、BeautyUnity、UnityDependencyBy
10、項目運營
自動構建
版本號——主版本號.特性版本號.修正版本號.構建版本號
[構建版本號]應用分發平臺升級判斷基準
自動構建
公司內部接入SODA即可,建議搭建自己的構建機,開發期間每日N Build排隊會死人的,另外也可以搭建自己的搭建構建平臺
統計上報
Tlog上報
玩家轉化關鍵步驟統計(重要)
Ping統計上報
游戲業務的統計上報(例如桌球球局相關的統計上報)
燈塔自定義上報
運營模板
上線前的checklist
項目 | 要點 | 說明 | 指標 |
燈塔上報 | 1. 燈塔自帶統計信息2. 自定義信息上報 | 燈塔里面包含很多統計數據,需要檢查是否ok | 1. 版本/渠道分布 2. 使用頻率統計 3. 留存統計(1天留存、3天留存、7天留存、14天留存) 4. 用戶結構統計(有效用戶、沉默用戶、流失用戶、回流用戶、升級用戶、新增用戶) 5. 硬件統計(機型+版本、分辨率、操作系統、內存、cpu、gpu) 6. Crash統計(Crash版本、Crash硬件、Crash次數等) 等等 |
信鴿推送 | 能夠針對單個玩家,所有玩家推送消息 | ||
米大師支付 | 正常支付 | ||
安全組件 | 1. TSS組件接入 2. 隱藏內部符號表:C++開發的代碼使用strip編繹選項,抹除程序的符號 3. 關鍵數據加密,如影子變量+異或加密算法 | 根據安全中心提供的文檔完成所有項 | 接入安全組件,并通過安全中心的驗收 |
穩定性 | crash率 | 用戶crash率:發生CRASH的用戶數/使用用戶數啟動crash率:啟動5S內發生crash用戶數/使用用戶數 | 低于3% |
弱網絡 | 斷線重連考慮,緩存消息,重發機制等等 | 客戶端的核心場景必須有斷線重連機制,并在有網絡抖動、延時、丟包的網絡場景下,客戶端需達到以下要求: 一. 不能出現以下現象: 1、游戲中不能出現收支不等、客戶端卡死/崩潰等異常情況; 2、游戲核心功能(如登錄、單局、支付等)不能有導致游戲無法正常進行的UI、交互問題; 3、不能有損害玩家利益或可被玩家額外獲利的問題; 4、需要有合理的重連機制,避免每次重連都返回到登錄界面。 二. 需要對延時的情況有相應的提示 | |
兼容性 | 通過適配測試 | ||
游戲更新 | 1. 整包更新2. 增量更新 | 特別說明:iOS送審版本支持連特定環境,與正式環境區別開,需要通過服務器開關控制 | |
性能 | 內存、CPU、幀率、流量、安裝包大小 | 【內存占用要求】 Android平臺:在對應檔次客戶端最低配置以上,均需滿足以下內存消耗指標(PSS): 1檔機型指標:最高PSS<=300MB (PSS高于這個標準會影響28%用戶的體驗,約1800萬) 2檔機型指標:最高PSS<=200MB(PSS高于這個標準會影響45%用戶的體驗,約3000萬) 3檔機型指標:最高PSS<=150MB(PSS高于這個標準會影響27%用戶的體驗,約1800萬) iOS平臺:在對應檔次客戶端最低配置以上,均需滿足以下內存消耗指標(PSS): 1檔機型指標:消耗內存(real mem)不大于250MB(高于這個標準會影響53%用戶的體驗,約1900萬) 2檔機型指標:消耗內存(real mem)不大于200MB(高于這個標準會影響47%用戶的體驗,約1700萬) 【CPU占用要求】 Android平臺:CPU占用(90%)小于60% iOS平臺:CPU占用(90%)小于80% 【幀率要求】 1檔機型(CPU為四核1.4GHZ,RAM為2G)或以上機型:游戲核心玩法中,最小FPS應不小于25幀/秒 2檔機型(CPU為兩核1.1GHZ,RAM為768M)或以上機型:游戲核心玩法中,最小FPS應不小于25幀/秒 3檔機型(CPU為1GHZ,RAM為768M)或以上機型:游戲核心玩法中,最小FPS應不小于18幀/秒 【流量消耗要求】 游戲核心玩法流量消耗情況(非一次性消耗)應滿足以下條件: 1.對于分局的游戲場景,單局消耗流量不超過200KB 2.對于不分局游戲場景或流量與局時有關的場景,10分鐘消耗流量不超過500KB |
小游戲是近兩年業內熱門的趨勢,但對于已經有了App的休閑游戲而言,如果把小游戲版本也做到成功呢?《騰訊桌球》小游戲版的涅槃重生過程或許可以給同行們帶來一些啟示。
據騰訊游戲光子蠟筆工作室總經理王旭新在7月10日的微信公開課上海站演講時演講時透露,《騰訊桌球》小游戲版本上線50天最高日活躍用戶達到了520萬,而且月流水過千萬,同時還帶動了App版本的拉新、活躍和收入增長,增幅達到30%,這究竟是如何做到的呢?
以下請看GameLook整理的演講內容:
王旭新:在講小游戲之前,我想回顧一下,因為《騰訊桌球》在之前是有一個App版本的,大家可以看到這個圖我本人看到是比較傷心的,漲的這么高的一個DAU怎么就慢慢掉下來了。
其實峰值能夠沖的這么高,至少說明它是一款好的游戲,證明用戶比較喜歡這個玩法。但是它的用戶掉的這么快,非常休閑的競技手游用戶付費對于精品平臺來說不是非常友好的,所以發布以后的表現就會得不到平臺的持續支持,我覺得這是非常正常的事情,我們不能怪平臺,只能怪沒有更好的平臺出現。
當然我們也看到,受到吃雞、王者等超級手游的影響,我們的流量也會減少,但比較好的事情就是我們現在有了微信小游戲平臺。其實我們年初在考慮做這件事情的時候也是比較猶豫的,但幸好我們做了,從圖中這個曲線來看實際上是比較好的,我們團隊才投入了2個策劃、3個前臺、2個后臺和2個美術,研發時間只用了3周,就有了非常好的成績。
上線50天的日活躍最高達到了520萬,內購+廣告收入每個月過千萬,同時還帶動了App版本的增長,對新增、活躍及收入的提升大概有30%左右。
復刻還是創新?小游戲立項的思考
那么,我們在做這個小游戲立項的時候是怎么考慮的呢?
因為這款游戲發布App版本的時候是很成功的,我們如何保證在小游戲平臺也能成功呢?說不好聽點,如果上線跪了,也是很沒有面子的。我們面臨的選擇是,在App的基礎上復刻,還是在原來的基礎上對小游戲版本再做一點小小的創新。
我們是怎么思考創新的呢?功能我們已經有了,但是用戶是怎樣的?微信平臺的用戶非常喜歡分享,但它還是弱中心化的。從用戶玩游戲習慣來看,小游戲用戶都是用碎片時間,而且都是非常輕度的玩家,具有非常好的社交性,所以我們還是決定做一些思路的改良。
玩過我們App版本的可能都知道,《騰訊桌球》實際上有很多種玩法,其中一個比較代表性的就是比賽模式,它有很多重度用戶在玩,但這個模式在App版本基本上是沒有社交的,你要分享出去,可能好友根本就不會理你,所以達不到社交的目的。從玩家數據來看,我們App版本的用戶是很重度的,日平均在線時長達到80分鐘以上,而整體小游戲的日均在線時長還不到40分鐘。
最后一個是商業化的差異,App版本實際上是比較重度的,小游戲的用戶是輕度休閑玩家,我們不希望商業化影響到用戶體驗,基于以上原因,我們對小游戲版本做了很大的改變。
以社交為核心、重新構建玩法和商業化設計
我們想以社交為核心,重新構建所有的玩法和商業化設計。當然我這里說的重做并不是完全推翻重來,而是我們需要的功能可以達到影響用戶的目的,所以我們在做每個功能的時候都會考慮到以下幾點:
第一個,玩家在體驗這個游戲的時候是否有動力傳播我們的玩法,這個是很重要的;第二,就是我們這個玩法有沒有盡可能的挖掘小游戲平臺提供的能力;第三個,這個系統或者玩法是不是足夠被更碎片和輕度的玩家所接受,玩家屬性決定了我們不能做非常重度的游戲,至少現在是不行的,或者以后可以。最后一個,iOS暫時還是沒辦法付費的,我們后續也會考慮這個事情。
有了這個核心概念之后,我們比較好的做了版本規劃。大家做任何一款游戲,首先要做的就是核心玩法的打磨,包括剛才分享的《海盜來了》,我們必須把基礎的玩法打磨好,才能進行下面的工作。基礎體驗打磨好之后我們會完善核心玩法,再之后是探索社交內容,這樣才能進一步利用平臺優勢做持續的探索。
可能大家看到這張圖會比較迷惑,這實際上是我們的手感編輯器。有人覺得做桌球游戲挺容易的,但是如果你想調出一個比較好的手感,盡量還原現實手感的話是很困難的一件事。所以我們專門寫了一個工具進行調試,并且找了專業的玩家對手感進行打磨,有了這樣的工具以后,我們的手感可以和競爭對手形成比較大的壁壘,保證核心競爭力,確保我們這個產品是OK的。而且當你把手感做好之后,用戶去體驗別的游戲就會感到不適應,這樣就不會輕易流失到競品游戲中,我覺得這也是非常重要的。
完善核心玩法,社交是新的引爆點
有了好的玩法之后,我們也需要好的包裝,因為大家都是喜歡美的,如果只是有一個核心玩法,而沒有好的視覺體驗,你的產品是非常難往前走的,而且現在的手游用戶對美的要求越來越高,如果視覺效果做的更好,可能商業化的路子就能走的更遠一些,有了這些以后,我們的小游戲版本基本上可以和App版本做到完全一樣的視覺效果。
對于老用戶來說,如果小游戲版本視覺效果不如App版本,他是不會玩的;如果是新用戶,他看到你的視覺效果做的不好,可能也不會玩。
說了這么多,其實基礎玩法一直是我們的立命之本,但除了基礎玩法和視覺表象,社交也是我們小游戲的一個特點,比如這兩張圖看起來差不多,實際上很多經典玩法在小游戲里都是有的,比如經典黑八、花式九球、競技斯諾克等等。
但是我們花了很多精力重點在新的社交玩法,例如單人無盡模式,這個和App版本是不一樣的,它的上手門檻地,可以更好的鼓勵玩家分享和炫耀。第二個,好友約戰我們也很重視,是放在首頁的,因為這樣可以讓用戶看到,你很容易和朋友一起玩,也證明了這樣一個功能可以在微信小游戲里發揮到它的功效。在App版本里面,其實沒有多少人真正去約戰,但在微信小游戲里面,好友約戰實際上占了我們用戶的30%,帶來了非常好的傳播效果。
第三個是我們剛剛出的花式闖關,讓用戶去產出內容,我覺得這是未來的一個趨勢,因我們不可能有非常多的時間和精力不斷完善內容,我們還是要讓玩家創造內容并且傳播,他們的想法永遠是無窮無盡的。
有了上面的這些內容,我們實際上也做了很多的方法,包括好友約戰、邀請活動以及游戲推薦,這些都是我們常規的方法。
配合這些方法,我更想強調的是這一個,小游戲的版本發布成本很低,而且玩家基本是無感知的,我們都知道iOS版本審核比較久而且大版本大版本做起來比較累,有了小游戲之后,我們可以更好的規劃周版本和月版本,處理外網的反饋,試錯的時候也可以用小版本進行迭代。
所以我們上線以來每周會有一兩個小版本,每個月會有一個大版本,而且可以建立完善的數據統計分析機制,不斷地的做優化和處理。
在分享方面也做了很大的挑戰,大家看到第一個圖是我們第一版的分享,大家可能看到很多小游戲都會用,我們也用了,但實際上轉化率非常低,整個拉新比例才16%。
當我們感覺這種方法太低了,就去想更好的方法去做,所以做第二版的時候,我們想在游戲和用戶之間產生感情,加入更多核心玩法元素,做了第二版之后,我們的新進用戶比例達到了50%;但50%也不能就滿足了,如果后面沒有資源了也不是個辦法,所以就改了第三版,還是以核心玩法為基礎,進一步提高與玩家的感情,讓他們在分享的時候感覺到有機會展現自己的能力,所以第三版之后,通過分享拉新占新進比例占到了75%,這就是一個非常健康的水平。
小游戲的商業化設計:平衡安卓與iOS體驗
前面分享了一些的核心玩法和體驗,最后還是回歸到商業化的合作,做一款游戲如果不賺錢也是不行的。我們面臨比較大的一個問題是,iOS是沒辦法內購付費的,對于這樣一個全區全服的游戲,很重要的一個事情就是,要考慮平衡不同終端玩家的體驗,讓用戶感覺到安卓和iOS是比較公平的,不然玩家總會有些抱怨,雖然不是每個用戶都付費,但如果想付費都付不了,就是非常不友好的。
所以在安卓版本,我們提供了內購和廣告激勵,但在iOS版本,我們真的沒辦法加付費,純粹靠廣告驅動。
做了這個之后,我們還需要考慮的問題是,怎么平衡道具價格之間的差異,剛才分享的時候也提到,小游戲用戶是比較新的,他們的付費能力暫時還沒達到普通玩家的水平,這是必須要考慮的,所以我們還是對道具價格進行了適當下調,而且也讓付費壓力減少了很多。
例如我們在App版本當中一個球桿的升級過程非常漫長,而且付費門檻略高,但我們在小游戲里做了處理,讓這個升級更短平快一些,付費門檻也大大降低。
在安卓里面,我們也要做道具付費的平衡,因為安卓版本已經加入了內購,讓他們在看廣告的時候盡可能的得到一些裝飾性的道具可以炫耀,但iOS是沒辦法做到平衡的,所以iOS版本的廣告里面提供了一些有價值的道具,但是由于涉及到商業的東西,我們還是對這些珍貴道具進行了嚴格的控制,但至少讓用戶有能力獲得想要的東西還是很重要的,所以我們在做小游戲的時候必需要考慮到iOS和安卓的平衡,特別是全區全服的游戲。如果是分區分服,這個問題其實基本上沒有的。
總結:做小游戲的三個重點
最后做一個總結,我們從三個方面介紹小游戲的研發:
第一個是玩法,你做任何一個游戲,玩法都是必需要考慮的。對于小游戲來說,它有億級用戶,而且前景可觀,但難點在于,小游戲玩家更碎片化,更適合輕度和休閑競技類的游戲。
第二個是社交,如果我們做小游戲沒辦法用好社交這個功能的話,我相信是不可能做大的。但是做社交的時候,也要控制好內容,傳播太多了是非常不好的事情,也會影響到整個生態,做有價值的傳播,建立你的游戲和用戶之間的感情聯系,讓用戶真正的喜好這種分享。
第三個就是商業化,我們還是要考慮好怎么平衡安卓與iOS之間的區別,也要考慮到用戶情況,讓我們這款游戲能夠取得成功,謝謝各位。