如何优雅的管理游戏资源
在游戲的開發過程中,前期的規劃 往往比 后期的“優化”更為重要!比如多分辨率適配,如果前期沒有規劃好,可能導致的情況是,畫面只在當前測試開發機或者一部分機型正常顯示。做了多套資源適配,可以使在合適的機型使用對應的圖片資源,避免在高清屏幕使用低質量的圖片,在低分辨率屏幕因為圖片太大而浪費硬件資源。機制與策略分離,可以讓你設計出簡單有效的接口。模塊化的設計可以讓你組織好各種邏輯流程,條理分明 ~ 前期的規劃工作可以有很多,一葉也在摸索之中,以使游戲的開發盡量變的簡單靈活且可控。最簡單的也是最容易忽略的地方,跟我們打交道最多的要數精靈了,從圖片創建一個精靈,很簡單的開端,將以此展開行動 ~
本文使用 Cocos2d-3.0版本,創建了一個 C++ 項目,介紹在 C++ 中,如何處理資源相關的內容,如果讀者使用腳本,也可以參考本文中資源管理理念而忽略語言特性,你可以在 Github[^1] 上面看到本文所有源碼。
名字系統
也許你可稱之為“命名規范”,但顯然它無法表達我所想說的內容,很多人在創建精靈的時候喜歡直接使用資源名稱,而沒有任何定義,這是一個不好的習慣,如果游戲資源不存在,缺少,或者修改名字,如此你需要在多出引用的地方一一修改。游戲開發中的變數總是無法預料,合理的“名字系統”可以節省很多人力。
我們設定一個文件,這里名為 “Resources.h” 的文件,在其中定義所有的資源名稱,在游戲開發中,盡量只?使用此處的名稱,如圖片名稱,字體名稱,聲音資源等。這樣做有以下好處,只是簡單說幾點:
- 如果對資源做出修改,我們可以修改此處定義,以保證同步,避免缺失,命名錯誤,錯誤引用等問題
- 在圖片名定義修改時,編譯器會編譯出錯,并自動幫助我們 “找” 出引用的地方,方便修改
- 由于有常量定義的緣故,我們的 IDE 會自動補全所有以定義變量名稱,減少出錯的可能,提高效率
- 這個文件列表顯然可以寫一個如 python 腳本自動生成
使用腳本來自動生成文件常量定義顯然是個行之有效的途徑,這種機械式的操作交給腳本就行了,它總能出色的完成任務,首先來看看項目的 Resources 目錄內容:
<code> [Resources]~ ./tree ├── CloseNormal.png├── CloseSelected.png├── HelloWorld.png├── file_list.json├── fonts│ └── Marker\ Felt.ttf└── images├── JungleLeft.png├── ghosts.plist├── ghosts.png├── grossini_family.plist└── grossini_family.png</code>
以上是資源文件,那么通過腳本所生成的 “Resources.h” 文件又是什么樣子的呢,腳本在 Github 倉庫中可以找到(注意:資源名中最好不要有空格,以免留下“隱患”):
#ifndef _AUTO_RESOURCES_H_#define _AUTO_RESOURCES_H_// search pathsstatic const std::vector<std::string> searchPaths = {"fonts","images",};// filesstatic const char si_CloseNormal[] = "CloseNormal.png"; static const char si_CloseSelected[] = "CloseSelected.png"; static const char sjs_file_list[] = "file_list.json"; static const char si_HelloWorld[] = "HelloWorld.png"; static const char st_MarkerFelt[] = "Marker Felt.ttf"; static const char sp_ghosts[] = "ghosts.plist"; static const char si_ghosts[] = "ghosts.png"; static const char sp_grossini_family[] = "grossini_family.plist"; static const char si_grossini_family[] = "grossini_family.png"; static const char si_JungleLeft[] = "JungleLeft.png"; #endif // _AUTO_RESOURCES_H_
看到通過腳本,我們生成了所有文件的常量定義,這讓得我們可以在游戲中任意使用,但是請注意,這里生成的文件名稱是沒有包含路徑的,所以在定義文件之前,也自動生成了目錄列表?searchPaths,顧名思義,設定了一個目錄列表,以便找尋資源,我們可以在程序的開始處使用?FileUtils::getInstance()->setSearchPaths(searchPaths);?來設定游戲的資源目錄列表,這樣我們就可以不用關心資源所在的目錄了,你甚至可以根據需要合理的調整資源目錄。
注意:通過設置 searchPaths 可以讓我們不用關系資源的路徑所在,那么意味著資源名稱必須唯一,否則可能會出現引用問題。其次,是如果使用了多套資源方案,請注意 searchPaths 的先后順序關系。本文暫不考慮多套資源。關于忽略資源目錄的做法,如果有不同看法者,歡迎留言討論,對我來說,忽略路徑是利大于弊的 ~
以上通過腳本自動生成了文件列表,但是這顯然不夠,我們看到資源當中有兩張?打包?資源圖片(可以使用 TexturePacker 對圖片資源進行打包,具有占用更小空間,優化運行效率等諸多好處,后面還會介紹此點) plist 文件。我們當然也是需要使用打包中資源的,所以腳本需要能夠自動解析 plist 文件,并提取出 TexturePacker 打包的資源名稱,請看如下定義,同樣是自動生成在 “Resources.h” 文件之中:
// texture //// ghosts.pliststatic const char si_child1[] = "child1.gif"; static const char si_father[] = "father.gif"; static const char si_sister1[] = "sister1.gif"; static const char si_sister2[] = "sister2.gif"; // grossini_family.pliststatic const char si_grossini[] = "grossini.png"; static const char si_grossinis_sister1[] = "grossinis_sister1.png"; static const char si_grossinis_sister2[] = "grossinis_sister2.png";
此時我們就能用以下代碼來創建精靈了,都引用了資源名稱定義,并且使用兩種方式創建了精靈:
// 直接由圖片創建精靈auto hello = Sprite::create(si_HelloWorld);// 從打包資源創建精靈SpriteFrameCache::getInstance()->addSpriteFramesWithFile(sp_grossini_family, si_grossini_family);auto sister = Sprite::createWithSpriteFrameName(si_grossinis_sister1);上面我們使用兩種方式創建精靈,為什么會有兩種方式?也許你可以看看?『子龍山人』?翻譯的文章?『在cocos2d里面如何使用Texture Packer和像素格式來優化spritesheet』?其中詳細的介紹了圖片資源打包優化的相關細節問題,一個游戲最多的就是圖片資源,優化空間最大的也是圖片資源,里面詳細的介紹了優化圖片資源占用空間 50% 以上,如何使游戲運行內存占用優化近 50%,以 cocos2d 為例,但 cocos2d-x 同樣能夠適用,而且能通過腳本自動打包。 所以合理的對圖片資源進行打包優化是非常有必要的。但如何處理這個流程確實不好定奪,因為不同資源的使用方式不同,因為這兩種方式的存在,導致我們編寫代碼的邏輯不同,這需要提前預定好,所以我們考慮如下開發流程:
在游戲開發前,對所有資源打包后提供給 編寫游戲人員,也就是說在寫程序之前,游戲資源就已確定,那些以打包,哪些未打包都已經知道,如前面一樣,通過兩種方式創建精靈。但是這樣的結果是,前期規定好了的,后期就無法改動,或者說很難改動,牽一發而動全身啊 ~ 這就需要加大?前期的規劃?力度,以確保后期不會出現太大太多事與愿違的情形。這種情況下的?后期優化?將會非常蹩腳。況且加大前期規劃的力度,可能會對整個項目的進程有所影響,如比編寫人員的動工會稍緩,人力資源分配不合理。
透明
前文提到,我們使用了 searchPaths 變量,以用忽略資源的路徑,一個存在的東西,看起來好像不存在一樣,我們稱之為 “透明”,”透明” 在軟件領域中也是重要的概念,它也強調著封裝的重要性,隱藏細節的必要性。這里的資源路徑就是如此,我們可以說?對于資源的使用來說,它的路徑是透明的,有沒有路徑,路徑為何?那不重要,重要的是你能通過資源名稱獲取想要的資源。
也許你已經發現了,我想說的不是路徑問題,而是圖片資源問題。對于圖片資源的使用來說,它’是否是打包資源’ 應該是透明的。也既是在使用圖片資源的時候,你不應該關心它是不是打包后的資源,是也好,不是也罷,這不應該影響你對資源的請求和使用。”打包” 這個過程對你來說,不存?~
圖片資源類型的 “透明化” 處理
先來段代碼,看看沒有 “透明化” 處理時的一般使用方式:
// 方式一 文件資源auto jungle = Sprite::create(si_JungleLeft);// 方式二 打包資源SpriteFrameCache::getInstance()->addSpriteFramesWithFile(sp_grossini_family, si_grossini_family);auto sister = Sprite::createWithSpriteFrameName(si_grossinis_sister1);以上我們看到,同樣是創建精靈,?si_JungleLeft?是普通的?文件資源,而?si_grossinis_sister1?是?打包資源,這決定著兩者的使用方式不同,那么怎么 “透明化” 處理呢:
// 不論圖片屬于 文件資源 還是 打包資源 使用方法相同auto jungle = AssetLoader::createSprite(si_JungleLeft);auto jungle = AssetLoader::createSprite(si_grossinis_sister1);
我們提供了一個類?AssetLoader,它有一個方法?createSprite(const std::string& name)。不論我們是不是打包資源,我們都通過這個方法來創建精靈,顯然它的內部工作原理是根據圖片的實際類型,動態判斷并創建,之后返回,要實現這樣一個功能是可行的,并且沒有多復雜,實現以后。我們在使用圖片資源的使用就再也用關心它是什么類型的資源了。
這也意味著你可以以一個理想的方式來管理開發流程。?圖片資源可以和游戲編寫同時進行,不停的添加圖片資源,不停的編寫游戲邏輯,而不用考慮圖片是否已經優化的問題了,此時可以提供一些零散的圖片,以供使用(圖片命名最好還是固定),當然也可以提前把關聯性比較強的圖片提前打包處理,這并不影響使用,因為對程序來說,它?是透明的,”不存在”的。在后期,我們可以集中的在后期對游戲資源優化,打包處理等(關于此點,文章后面也會給出相對合理的處理流程)。
功能的實現方案與流程
在開始之前,一葉通常會將其流程在心中演算一遍,使其不會出現太大的紕漏,對于不合理的所在,可以重新擬定方案。然后實現之 ~ 要使得 AssetLoader 的 createSprite 方法完成其功能,那么它需要知道,當前請求的?圖片資源?是否是?文件資源(以?文件資源?和?打包資源?區分兩者),如果是,直接由前文?方式一?創建精靈返回,如果不是,則從 打包資源 里面找尋,找到就通過?方式二?創建精靈并返回,如果還沒找到,就返回空指針嘍 ~ 由此我們知道 AssetLoader 它內部需要完成以下功能:
- 能夠判斷一個資源是否是文件資源
- 能夠根據打包資源圖片名稱返回實際的 plist 文件(打包資源描述文件)和 圖片文件
要完成以上功能,那就需要讓 AssetLoader 知道有哪些文件資源,還要知道有哪些打包資源,我們可以在 AssetLoader 里面定義幾個字典用以保存這些數據:
class AssetLoader: public Object{public:static Sprite* createSprite(const std::string& name);private:static AssetLoader* getInstance();bool init();bool fileExists(const std::string& filename);std::string getTexturePlist(const std::string& name);std::string getTextureImage(const std::string& name);private:Dictionary* _fileDict; // 文件列表Dictionary* _texturePlistDict; // 打包資源到文件的映射Dictionary* _textureImageDict; // 打包資源plist 到圖片的映射};
如以上的定義實現,它有三個字典,?_fileDict?的 key 保存著所有文件資源,value 保存文件資源的?編號?,這樣我們就能夠隨時判斷一個圖片是否是文件資源了。_texturePlistDict?的 key 保存著打包資源的名稱,value 保存打包資源所在 Plist 文件的編號,通過它我們能通過打包資源獲取到它 Plist 所在的文件。?_textureImageDict?也是類似,key 保存打包資源名稱,value 保存打包資源所在的真實 圖片文件的引用。
功能已經定義完畢,現在的問題是我們如何去為這幾個字典填充數據?顯然程序初始化手動填充不靠譜,前文的文件名等信息都已經是自動定義了,此處我們當然也希望有一個方案?自動填充?了。這里的做法是,在使用 python 生成資源定義的時候,同時生成一個 json 文件,這個文件里面包含了所有此處字典中所需要的數據,然后 AssetLoader 初始化的時候讀取這個 json 文件,以完成自動填充數據的功能。先來看看自動生成的 json 文件長什么樣紙:
題外話:使用 json 來存儲這樣一個中轉的數據格式是最后定下來的方案,設計之初考慮過幾種方案,比如想到可以用一個 sqlite 數據來保存各種數據,這樣數據的操作就非常統一,對后期的數據統計分析也會非常方便,曾與朋友 子龍山人 討論過這之間的詳細細節,以及各種實現方案的利弊分析。使用 sqlite 的好處是更為靈活,后期擴展功能會非常方便,適合稍微大點的項目,但是如果一個項目本身沒有使用 sqlite 數據庫,如果為這里的方案而硬添加一個擴展庫實現 sqlite,可能就會非常的不友好,不通用。
<code>{"file_name": ["CloseNormal.png", "CloseSelected.png", "file_list.json", "HelloWorld.png", "Marker Felt.ttf", "ghosts.plist", "ghosts.png", "grossini_family.plist", "grossini_family.png", "JungleLeft.png"],"file_index": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], "texture_name": ["child1.gif", "father.gif", "sister1.gif", "sister2.gif", "grossini.png", "grossinis_sister1.png", "grossinis_sister2.png"], "texture_plist": ["6", "6", "6", "6", "8", "8", "8"], "texture_image": ["7", "7", "7", "7", "9", "9", "9"] }</code>以上是自動生成的 json 數據文件內容,為了在這里展示,做了點格式化和縮進,更為友好一點。通過?file_name?和?file_index?可以創建文件資源列表字典,通過?texture_name?和?texture_plist?可以創建打包資源和文件資源之間的映射,?texture_image?也是同樣。 file_name 包含了所有文件資源,file_index 為文件資源做了編號,這兩個數據項的個數是相同的。 texture_name 定義了所有打包資源的定義,texture_plist 和 texture_image 則保存了 打包資源所在的 plist 文件和圖片文件的引用,它們的數據項個數也是相同的。只要保證這里生成的內容沒有錯誤,那么我們就正確的將其填充到 AssetLoader 的字典里面,以實現想要的功能。
為什么數據會長這個樣子?一葉本來的設計 json 文件,多層嵌套更具描述性(各種對象,各種屬性,一目了然),但是發現 解析的時候稍顯麻煩,在 新版 cocos2d-x 的 gui 庫中,已經封裝好了一些常用的 json 解析功能,本著?拿來注意(盡可能的尋找可用的資源來簡化自身的流程) 的思想,為使解析過程簡單,所以數據格式就定義成那個樣子了 - =。現在只是用了五個數組(更平面化的數據組織,像是數據庫表),保存所有數據。只能說,這樣做是為了迎合代碼的編寫,讓本來復雜的 json 解析過程變得更為簡單。現在看來,但到也簡單清晰,看看填充字典的關鍵代碼實現(如果有其它更好的方式,修改也不麻煩,修改生成的數據格式,再修改代碼中數據的填充方法就行了):
JsonDictionary *jsonDict = new JsonDictionary();String* fileContent = String::createWithContentsOfFile(sjs_file_list);jsonDict->initWithDescription(fileContent->getCString());DictionaryHelper* dicHelper = DICTOOL;_fileDict = Dictionary::create();int file_idx = dicHelper->getArrayCount_json(jsonDict, file_name);for (int i = 0; i < file_idx; i++){std::string name = dicHelper->getStringValueFromArray_json(jsonDict, file_name, i);std::string index = dicHelper->getStringValueFromArray_json(jsonDict, file_index, i);_fileDict->setObject(String::create(index), name);}log("file count: %d", file_idx);_texturePlistDict = Dictionary::create();int texture_idx = dicHelper->getArrayCount_json(jsonDict, texture_name);for (int i = 0; i < texture_idx; i++){std::string name = dicHelper->getStringValueFromArray_json(jsonDict, texture_name, i);std::string plist = dicHelper->getStringValueFromArray_json(jsonDict, texture_plist, i);_texturePlistDict->setObject(String::create(plist), name);}log("texture count: %d", texture_idx);_textureImageDict = Dictionary::create();for (int i = 0; i < texture_idx; i++){std::string name = dicHelper->getStringValueFromArray_json(jsonDict, texture_name, i);std::string image = dicHelper->getStringValueFromArray_json(jsonDict, texture_image, i);_textureImageDict->setObject(String::create(image), name);}CC_SAFE_DELETE(jsonDict);這里能看到一些陌生的內容?JsonDictionary、DictionaryHelper類型和其操作方式,這里的使用方法不是本文的重點,有興趣的朋友看看源碼實現。以很簡潔的方式,填充了我們需要的字典數據內容。有了這些字典數據,我們就很容易的判斷一個圖片是否是文件資源了,如果是打包資源,也能夠很容易找出打包資源所在的 Plist 文件和 圖片文件,最后看一下?createSprite?方法的實現:
Sprite* AssetLoader::createSprite(const std::string& name){if (AssetLoader::getInstance()->fileExists(name)){return Sprite::create(name);}log("create sprite: %s", name.c_str());std::string plistfile = AssetLoader::getInstance()->getTexturePlist(name);std::string imagefile = AssetLoader::getInstance()->getTextureImage(name);log("plist: %s, image: %s", plistfile.c_str(), imagefile.c_str());if (plistfile != "" && imagefile != ""){SpriteFrameCache::getInstance()->addSpriteFramesWithFile(plistfile, imagefile);return Sprite::createWithSpriteFrameName(name);}return nullptr;}至此我們便完成了?圖片資源類型的 “透明化” 處理?。這樣一個解決方案,很好的解決了在開發過程中圖片資源的管理過程,后期優化,都不沖突。能夠通過此提供一個較為合理的開發流程。 本文所使用的源碼,腳本等都可以在 Github 上面找到 『https://github.com/leafsoar/resource-manager』,但是要清楚,我這里提供的只是按照我這種流程下來的一種實現而已,對于程序本身而言,也還有很多可以改進的所在 ~ 思路同樣,每個人實現的具體細節可能不一樣,不論你使用 C++ 還是腳本語言,都不影響你 “透明化” 圖片資源類型。
如何優雅的管理游戲資源
我們解決了一些問題,提供了一些解決方案,但總有更多的問題等著我們去解決,更多的優秀解決方案,好的工作模式,處理流程。我們會把開發中一些?變動?的所在找尋出來,對它靈活的處理,使它能夠適應各種不同的?險惡環境。哪些是不變的,哪些是容易變動的,盡量做到?以不變應萬變。現在新的問題和需求又來了,哈 ~
繼續前文內容,我們可以使用 AssetLoader 來加載圖片資源,創建精靈,實現游戲玩法邏輯等。但是我們通常會在一個場景進入時就預先緩存所有圖片資源(聲音資源亦是同樣),甚至在游戲開始時,預先加載所有的圖片資源,以?保證游戲畫面的流暢性。如果沒有預先緩存圖片資源,那么在游戲中用到的時候,實時加載可能?會使游戲畫面卡頓,這不是我們想看到的結果。如果一個游戲不大,資源總和也沒多少,那么可以直接在游戲開始時全部加載完畢,這種情況處理起來比較簡單,直接把所有資源加載就可以了。但是如今的游戲動輒幾十兆,幾百兆,顯然游戲資源一次性加載是不科學的,這時我們可以分場景,在加載一個場景的時候,清空前一個場景所使用的圖片緩存資源,然后預先加載當前場景的游戲資源,以達到最優的內存占用。
通常我們都是人為的,定義了一個方法在開始場景前做一些準備工作,清空緩存,預加載游戲資源,如這里有一個需要預加載的資源列表,而前文我們提到,在游戲開發的過程中,我們的圖片資源可能會有所改動,這就需要我們去?人為的同步去手動維護這個列表,而這樣的工作費時費力,還容易出現很多錯誤,如果我們能夠把這一步的操作自動化,根據實際情況生成其列表,并且列表資源的加載順序也是做過優化的(根據文件大小,或者分辨率大小,優先加載大的資源,使游戲減少因占用內存過多而崩潰的可能性),那將使我們能有?更多的精力花在更值得的地方。如果結合到本文之前的實現方案就是,在開始一個場景時,我們對 AssetLoader 做一個標記,在這個標記之后所請求的圖片資源都是當前場景的資源,我們可以在內部將其記錄下來,以任何方式都行,這樣我們就能夠非常容易的收集并生成當前場景所使用的圖片資源了。如果我們將這個列表做成動態可維護的,自動記錄以便下次運行時預先加載,這樣一種實現從邏輯上來說時可行。如何優雅得管理游戲資源?但是實現比?優雅?更重要,在實現的過程中,盡量使開發變得簡單,流程變得清晰,也是一葉努力的方向 ~
以上只是對預加載資源列表的動態維護,提供了一個簡單的思路,其中還有很多細節值得推敲。但我想實現這樣一種流程對游戲的開發是非常有幫助的,對于這個部分的內容,一葉還沒有給出一個具體的實現方案,但將繼續之前的流程往下實現,并分享在 Github 上面,同時你也可以參與進來。也算是在這里集思廣益,如果你有什么好的想法,對本文實現有什么改進,都可以一起交流。如果你遇到了相同的問題,也可以說說你是怎么處理這些問題的,歡迎分享 ~
[^1]: 本文源碼 Github 倉庫地址:https://github.com/leafsoar/resource-manager
總結
以上是生活随笔為你收集整理的如何优雅的管理游戏资源的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 本地连接Redis服务器
- 下一篇: 华为防火墙配置(防火墙基础)