Cocos2D手機游戲開發之優化篇
在這個手機游戲盛行已久的年代,一款產品想要博得更多用戶的喜愛就要在細節上做得更加到位。而游戲的優化在這里面起到了非常關鍵的作用。試想下,一款畫面和玩法都深受用戶喜歡的產品,卻只能在高端機子上面運行起來,或者就算運行起來也是各種卡頓、閃退,這樣的結果相信不是任何一個游戲人愿意看到的局面吧。
相比于PC游戲,手游在內存上基本可以說是錙銖必較,一款非常普通的android機子想要運行一款稍微龐大點的游戲,內存的限制是非常苛刻的。所以,管理好內存的使用有時候也是衡量一名游戲人的重要標桿。
一、內存優化
在游戲中,占用內存最多的無非就是圖片資源,所以如果可以從圖片資源上面進行優化,那么得到的收益將會是最大的。
1.1、資源占用
首先,先來看一下一張 144X144 的圖片在物理磁盤上面的占用的存儲空間大概是30KB,但是使用Cocos2D游戲引擎加載到內存里面,它需要占用至少256KB的大小。
主要的原因有以下兩個:
- Cocos2D在像手機申請紋理圖片內存的時候,只能將圖片的寬高尺寸以2的n次冪大小來計算。
- Cocos2D默認的紋理深度是32位。
對于第一點,由于144并非是2的次冪數,與之相近的2的次冪數分別是128和256。如果使用128,那么申請到的內存空間顯然無法存放 144X144 這么大的圖片,那么只好使用256來進行計算,也就是Cocos2D會把它當成 256X256 大小的圖片來申請空間,但是圖片本身沒有那么多數據,所以造成了內存空間的浪費。
關于第二點,Cocos2D使用32位的像素格式來保存像素信息,也就是圖片上的一個像素需要占用32位(bit)。一個字節(byte)又等于8位(bit),所以一個像素需要占用4個字節(32 / 8)的大小。
所以,可以輕易的得出 144X144 圖片占用的內存為:
【內存 = 圖片的寬 X 圖片的高 X 4byte】
也就是:
256 X 256 X 4 = 262144 byte
然后,1KB = 1024byte,所以它占用的內存大概是256KB(262144 / 1024)。
1.2、合理利用內存
根據上面1.1提到的兩點浪費內存的原因,我們就可以對癥下藥了。
對于圖片尺寸導致的內存浪費問題,我們可以將很多小張的圖片合成一張大張的紋理圖,讓大圖的尺寸等于2的n次冪,這樣我們就可以盡可能的去利用那些原先被浪費掉的內存。
合成大圖的工具有很多,我個人比較推薦使用TexturePaker,這個工具有mac和win版本的,而且操作和界面布局基本一致。關于TexturePaker的用法,網絡上面的教程很多,可以自行搜索。
這里要說的一點是Publish導出紋理集的時候要** 選擇格式為“Cocos2d plist(*.plist)”格式 **,這樣Cocos2D游戲引擎可以直接加載并解析。
不同的設備由于設備硬件的不同,可以加載的圖片的最大尺寸也不同,所以我比較建議最大的紋理集尺寸為 2048X2048 ,這樣可以保證在所有的主流設備上面都能夠得到正確的加載。
1.3、修改像素格式
圖片上面每個像素都有RGBA四個通道,RGB三個通道用于表示該像素的顏色值,A通道用于表示Alpha(透明)通道,每個通道默認使用8bit的內存來存儲數據,所以總共是32bit。
32位的圖片可以表示16,777,216種顏色,對于手機游戲,很多時候并不需要使用這么多種顏色,很多顏色肉眼也很難分辨出來。所以,32位的位圖本身也挺浪費內存的,但是值得慶幸的是,Cocos2D允許開發者手動設置像素格式,代碼如下:
-- 設置像素格式為RGBA4444(16位)
CCTexture2D:setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGBA4444)
【我使用的是quick-cocos2d-x,所以代碼的語法都是lua的。】
Cocos2D游戲引擎支持的像素格式如下:
- kCCTexture2DPixelFormat_RGBA8888
- kCCTexture2DPixelFormat_RGBA4444
- kCCTexture2DPixelFormat_RGB5A1
- kCCTexture2DPixelFormat_RGB565
【說明】:
1、kCCTexture2DPixelFormat_RGBA8888:默認的32bit像素格式。
2、kCCTexture2DPixelFormat_RGBA4444:16bit像素格式,RGBA每個通道僅用4bit來存儲數據,由于每個通道的內存減少了一半,所以能表示的顏色也會相應的變少。如果可以,我一般都盡量使用這種格式。
3、kCCTexture2DPixelFormat_RGB5A1:16bit像素格式,該格式分別使用5個bit的空間存儲RGB顏色值,然后只用1bit表示透明值,所以它表示的顏色會比kCCTexture2DPixelFormat_RGBA4444更加的豐富,但是在透明度上只能表示全透明和不透明,無法表示半透明。
4、kCCTexture2DPixelFormat_RGB565:16bit像素格式,該格式將16bit都用于表示顏色,沒有透明通道,所以沒有辦法表示透明像素,通常游戲的背景圖可以設置成這樣的格式。
除了上述四種格式之外,其實還有其它的像素格式,比如:8bit等,但是它們可能無法滿足我們對游戲畫面的需求,所以比較有用的就上述這幾種。
【注意】由于設置像素格式是對全局進行操作的,所以一旦修改紋理的像素格式之后,后面加載的紋理格式就全部變掉了,如果想要恢復32bit深度,需要再次進行設置。當然,已經被加載到內存中的紋理不受影響。
二、渲染優化
游戲都是刷幀實現更新的,Cocos2D游戲引擎默認幀數是一秒鐘60幀,也就是一秒鐘要更新60次。但是設備的運算性能總是有限的,所以如果一幀之內的運算量過大,那么游戲更新一次的時間就會越久,一秒鐘就無法達到60幀的更新速率,也就是我們常說的掉幀現象。對于Cocos2D游戲而言,一般幀率低于40幀,玩家就能明顯的感覺游戲運行不流暢,畫面卡頓。
2.1、批量渲染
Cocos2D在進行畫面更新的時候,繪制一個精靈節點(CCSprite)需要通過如下三個操作:
打開 -> 繪制紋理 -> 關閉
那么如果一個場景里面有10000個精靈對象需要繪制,一幀的更新在渲染時候就要進行30000次(3*10000)完整的繪制操作。雖然手游很少會有一個場景里面10000個精靈的情況,但這無疑是一個要命的問題。
好在Cocos2D游戲引擎可以使用CCSpriteBatchNode來批量繪制精靈,創建CCSpriteBatchNode對象的代碼如下:
local batchNode = CCSpriteBatchNode:create("node.png", 300)
創建CCSpriteBatchNode的時候需要兩個參數,第一個參數是要批量繪制的精靈圖片路徑,第二參數是該CCSpriteBatchNode對象一次繪制的精靈個數【可以缺省】。
創建好CCSpriteBatchNode的實例對象后,就可以根據CCSpriteBatchNode的對象來創建精靈了,代碼如下:
local node = CCSprite:createWithTexture(batchNode:getTexture())
從CCSpriteBatchNode的實例對象中取得紋理對象,然后根據該紋理對象創建精靈。將創建的精靈添加到CCSpriteBatchNode對象上,然后將CCSpriteBatchNode實例對象添加到層(CCLayer)上面。這樣,創建的精靈便會被顯示在游戲場景中。
由于CCSpriteBatchNode使用的是同一份紋理,所以用它來進行批量繪制的操作如下:
打開 -> 繪制紋理 -> 繪制紋理 -> ... -> 繪制紋理 -> 繪制紋理 -> 關閉
使用CCSpriteBatchNode渲染10000個精靈還是需要進行10000次的繪制紋理操作,但是只需要打開和關閉一次,也就是省去了9999次的打開操作和9999次的關閉操作。
如果游戲開啟FPS顯示的話,明顯的可以在左下角看到繪制次數從10000變成了1,而且幀率也會有所提升。
2.2、多紋理批量渲染
由于CCSpriteBatchNode創建的時候只能指定一張紋理圖,所以在使用上面有一定的限制。比如有多個不同的紋理要進行批量繪制的時候,我們只好創建不同的CCSpriteBatchNode對象。
但是還有一種方法可以解決這種問題,那就是將多張需要批量繪制的紋理圖片合成一張大的紋理圖集。然后使用這張大圖來進行創建CCSpriteBatchNode,這時候創建精靈會默認顯示整張紋理,通過調用CCSprite的setTextureRect()方法,我們可以設置精靈只顯示紋理的一部分區域。
2.3、圖片格式
Cocos2D游戲引擎支持png和jpg格式的紋理圖,但是比較推薦使用的是png格式。因為Cocos2D在加載jpg格式的紋理時,會實時轉換成png格式。這表示加載jpg格式的紋理會比加載png格式的紋理要慢,并且轉換的過程內存也會成倍的增加(3倍)。
2.4、其它方式
除了使用CCSpriteBatchNode來進行批量繪制外,如果在加載紋理時將像素格式設置為16bit深度的話,大概也可以提示10%左右的渲染性能。畢竟,處理32bit深度的位圖需要更多的運算。
三、資源加載/卸載優化
3.1、資源加載
如果打開的游戲場景內部使用資源比較大,那么在構建場景的時候就需要讀取更多的圖片資源到內存中,IO操作本身就是一個耗時的操作,量大的話很容易導致游戲場景跳轉時卡頓。
在場景構建時如果要讀取比較多的資源,我們通常會添加一個loading(加載)場景,然后在loading場景里面做資源的加載操作,等資源全部都加載到內存中的時候,再跳轉到需要打開的場景,訪問內存空間的速度要遠遠高于訪問磁盤空間的速度,所以這時候跳轉場景就會很快速了。
很多程序員在進行資源預加載的時候經常出現程序崩潰的想象,有時候還會問為什么明明做了資源預加載,為什么一進入那個場景就閃退了。
閃退的原因有很多,比如:代碼本身有bug、資源被釋放導致的空指針、內存驟升等。
1、對于代碼本身的bug,我只能說很遺憾慢慢去找吧,改掉就好。
2、資源被釋放導致空指針問題,這個挺常見的,有時候兩個場景有一部分資源是共用的,但是上一個場景結束的時候就給釋放了,導致下一個場景要使用資源的時候方向是個空值(NULL)。對于這種情況,我們需要關注的是兩個場景進行跳轉時生命周期的回調順序,在合適的地方進行釋放應該問題就不大。
3、內存驟升,OS(操作系統)在發現某一個進程如果內存突然間急劇上升,很有可能會將該進程kill掉。
進程會被操作系統殺死,這種事情我們是無力回天了,但是我們可以搞清楚內存為什么會急劇上升。
Cocos2D在加載紋理圖片的時候會先創建CCImage對象,然后根據CCImage對象去創建CCTexture2D紋理對象。所以,這個時候內存的開銷是翻倍的。當創建好CCTexture2D紋理對象后,CCImage才開始釋放。如果加載的紋理圖片格式是jpg的話,由于需要進行格式轉換,內存的開銷就變成是三倍。
當我們在進行資源加載的時候,如果是一張接著一張的加載,有些CCImage對象沒有及時的得到釋放,內存淤積,這時候就會導致內存上升過快。一個有效的解決方案是在加載完一張紋理后,延遲一兩幀再加載下一張紋理。延長加載的時間間隔,讓CCImage得到有效的釋放,內存就不會上漲的那么快。當然,加載的時間也會變得更長,我通常是延遲2幀,loading的時間總體上還是可以接受的。
3.2、資源卸載
進行資源卸載的時機是非常重要的,如果資源被占用,那么遍無法被有效的卸載掉,如果資源在使用前被卸載也很有可能導致空指針問題。通常我們可以在場景跳轉的時候進行一次資源卸載,但是要等舊場景完全被釋放后,新場景又創建結束之后進行資源卸載。這時候由于舊場景被釋放了,占用的資源也可以得到有效的釋放,新的場景又創建完畢,很少出現場景創建過程中讀取紋理失敗的問題。【要了解場景的生命周期】
3.3、分步加載/卸載
對于很多大型的游戲,由于游戲本身資源量太大,導致無法全部加載,這時候可以在合適的地方進行分步加載,這樣游戲打開的速度便會得到有效的提升。同事,可以釋放一些沒有用的資源,合力的利用內存。
但是資源的加載和卸載比較是IO相關的操作,其本身就會耗費較多的性能。在進行加載和卸載的時候會影響到游戲的流暢度,所以在條件允許的情況下,能一次加載的絕不分多次加載,這樣可以保證游戲的流暢度,增強體驗感。
四、安裝包大小優化
游戲安裝包的大小很大程度上決定了游戲的下載量,除非這款游戲真的十分出色。
現在手機的移動流量費都非常的昂貴,每個月的套餐流量又十分的有限,雖然遍地有WIFI。但是包體越大,下載的時間就越久,手機游戲就跟快餐一樣,都是閑暇時間玩個兩三分鐘的那種,如果游戲包很大,下載過程中可能就取消不玩了。
4.1、TinyPNG
在游戲開發完成后,我們可以使用TinyPNG等工具來進行圖片文件的壓縮。TinyPNG有PS插件和網頁在線兩種方案可供選擇,PS插件需要收費,我們可以使用網頁進行壓圖操作,網址是:https://tinypng.com/。
TinyPNG可以將圖片壓縮為原來的50%左右,壓縮量非常可觀,而且雖然是有損壓縮,但是壓縮后基本看不出來。
4.2、降低圖片深度
說到這個,我們需要再次提到TexturePaker這款軟件,TexturePaker可以輕易的將圖片設置為16bit深度的格式。這樣圖片占用的空間也會減少一半。
但是使用16bit深度的圖片格式來表示原本32bit的圖片,很多顏色將會丟失,色值便會變成與之相近的另一種顏色。帶有過渡色的圖片便會出現明顯的梯度。
如上圖所示,可以明顯的在花瓣上看到條紋狀的顏色,這顯然不是我們希望看到的事情。好在TexturePaker這個工具支持紋理震蕩算法,我們可以在TexturePaker面板上設置Dithering屬性為“FloydSteinbergAlpha”,這時候就可以看到紋理顯示基本恢復正常了。
然后將圖片導出,但是要注意的是這時候雖然紋理圖片是16bit的,但是Cocos2D還是會默認將其當成32bit位圖進行加載,除非手動修改紋理的像素格式。