利用文件摘要简化游戏资源的引用管理
資源的引用管理是個(gè)有趣的話題,最近我在代碼里實(shí)踐了一種做法,可以在某些方面簡(jiǎn)化資源的管理,完成之后簡(jiǎn)單記錄在這里。這篇文章先介紹傳統(tǒng)的各種方式,然后簡(jiǎn)單說(shuō)明一下,這個(gè)實(shí)踐在傳統(tǒng)方式的基礎(chǔ)上做了哪些改善,解決了什么問(wèn)題。
引子
游戲開(kāi)發(fā)中的資源管理,通常是指針對(duì)游戲中的各類資源數(shù)據(jù) (模型,貼圖,腳本,數(shù)據(jù)表等等),通過(guò)合理安排布局來(lái)提高資源訪問(wèn)的效率,進(jìn)而改善游戲體驗(yàn)的過(guò)程。在布局方面的一些實(shí)踐,譬如“如何區(qū)分對(duì)待不同的資源類型,如何做到更新友好”等等,這里就不詳細(xì)討論了。今天主要談一下在大量資源已合理布局的情況下,如何有效地處置它們相互之間巨量的依賴和引用關(guān)系的問(wèn)題。
簡(jiǎn)單地說(shuō),如果 A 引用了 B,那么應(yīng)該如何簡(jiǎn)潔有效地表達(dá)這種引用呢?
有經(jīng)驗(yàn)的開(kāi)發(fā)者知道,這個(gè)問(wèn)題并不像看上去這么簡(jiǎn)單。隨著資源量的劇增,以及牽扯到的工作流程的細(xì)碎化,如果處置不善,資源引用問(wèn)題會(huì)成為影響整個(gè)架構(gòu)的根本性問(wèn)題。
傳統(tǒng)實(shí)踐
方式 I - 基于偏移 (指針) 的引用
文件偏移 (file-offset) 應(yīng)該是最基本最原始的引用方式了。在一個(gè)運(yùn)行著的 C/C++ 程序中,通常我們通過(guò)在對(duì)象 A 中存儲(chǔ)指針來(lái)引用對(duì)象 B。如果在序列化時(shí),把這種指針引用以文件偏移的形式直接寫入文件,就是最原始的資源引用管理。
?
這種最原始的依賴管理,細(xì)分一下還有兩種形式:
1.每個(gè)對(duì)象的地址 (&object) 被一并存下來(lái)用作該對(duì)象的 ID (順便保證了全局唯一),將引用者寫入文件時(shí),如果出現(xiàn)被引用者的指針,就直接寫入其地址。這么做的好處是簡(jiǎn)單直接,速度快,與運(yùn)行時(shí)地址空間一一對(duì)應(yīng),有時(shí)候甚至非常有利于調(diào)試。但缺點(diǎn)和限制是每個(gè)對(duì)象需要額外的4個(gè)字節(jié) (64位就是8個(gè)字節(jié)),而且必須保證在序列化的過(guò)程中不發(fā)生相關(guān)內(nèi)存的釋放和重新分配 (因?yàn)榭赡軐?dǎo)致同一地址被不同的對(duì)象“復(fù)用”了)。
2.每個(gè)對(duì)象在被寫入文件時(shí),使用當(dāng)時(shí)的文件偏移作為該對(duì)象的 ID (通過(guò)每個(gè)偏移在文件中的唯一性來(lái)保證全局唯一),將引用者寫入文件時(shí),如果出現(xiàn)被引用者的指針,就寫入其文件偏移。這么做省去了指針的存儲(chǔ)開(kāi)銷,但由于文件寫入是有先后次序的,先寫入的對(duì)象如果引用了后寫入的對(duì)象,此時(shí)還不知道文件偏移,就只有在第一遍寫完所有對(duì)象之后,再寫第二遍填上引用的空缺(或者是預(yù)先在內(nèi)存中把偏移算好)。
為什么說(shuō)這種方案很原始呢,因?yàn)橐粋€(gè)地址所能攜帶的信息太少了。在載入時(shí),我們必須在整個(gè)過(guò)程中都非常清楚自己在操作什么類型的數(shù)據(jù),這樣就需要大量額外的代碼來(lái)在不同的情況下創(chuàng)建不同類型的對(duì)象,這是非常繁瑣和易錯(cuò)的。究其原因,就是引用的信息量不夠,做不到某種程度的自描述。
關(guān)于打包的單獨(dú)討論
由于這種方案足夠的快,在一些游戲引擎的二進(jìn)制數(shù)據(jù)文件中有非常普遍的應(yīng)用。為了保證讀取效率,游戲引擎通常會(huì)把邏輯上相關(guān)的資源打包在一起,避免反復(fù)讀取零散的文件。由于在包內(nèi)的文件仍保持著與文件系統(tǒng)相一致的樹(shù)狀存儲(chǔ)結(jié)構(gòu),所以“物理包文件 + 虛擬的內(nèi)部文件結(jié)構(gòu)”,本質(zhì)上跟典型的OS樹(shù)狀文件系統(tǒng)并無(wú)不同。提供這種打包機(jī)制的引擎通常會(huì)把這一層給抽象掉,大多數(shù)情況下,游戲代碼仍像訪問(wèn)普通文件一樣去訪問(wèn)內(nèi)部的一個(gè)資源。這也就是在說(shuō),理想情況下,一個(gè)考慮周詳?shù)拇虬鼨C(jī)制,應(yīng)做到保留 OS 文件系統(tǒng)的基本語(yǔ)意,將其自身透明化,不破壞和干擾已有的文件訪問(wèn)方式。
出于簡(jiǎn)化討論的目的 (不影響討論的內(nèi)容和結(jié)果),我們將只討論基于傳統(tǒng)的 OS 文件系統(tǒng)下的資源相互引用問(wèn)題,而把“是否應(yīng)該打包,如何打包”等問(wèn)題正交地拆分出去,視作另一個(gè)維度的考慮。
方式 II - 基于路徑的引用
(形如 '/foo/bar/miracle.png')??
?
正如標(biāo)題里的例子那樣,按照路徑來(lái)索引資源,應(yīng)該是最自然和直觀的引用方式了。事實(shí)上,互聯(lián)網(wǎng)上的資源和服務(wù),大部分都是通過(guò) URL,以路徑方式來(lái)提供的。
?
使用路徑來(lái)索引資源時(shí),如有可能,應(yīng)當(dāng)盡量使用相同格式的歸一化的平臺(tái)無(wú)關(guān)的路徑。混用 '\\' 和 '/',使用 "/../" 或 "/./",等等,都會(huì)造成無(wú)法直接比較兩個(gè)引用是否指向同一份資源,而且對(duì)同一資源的引用字符串 hash 的結(jié)果會(huì)不一致。
當(dāng)需要移動(dòng)或重命名資源的時(shí)候,路徑就失效了。這時(shí)候,簡(jiǎn)單的做法是,總是在編輯器提供的資源管理工具中進(jìn)行 move/rename 的操作,這樣可以自動(dòng)更新所有對(duì)該資源的引用。涉及到全庫(kù)范圍的掃描和修改,當(dāng)資源量大時(shí)可能會(huì)非常慢。
一個(gè)常見(jiàn)的實(shí)踐是使用所謂的 "Redirector",當(dāng) move/rename 發(fā)生時(shí),在原來(lái)資源的位置放置一個(gè)跳轉(zhuǎn),指向新的位置,這樣所有的相關(guān)資源都可以保持對(duì)原資源的引用,無(wú)需被動(dòng)更新。在全庫(kù)范圍內(nèi),可以定期地運(yùn)行自動(dòng)化工具來(lái)清理這些跳轉(zhuǎn),手機(jī)號(hào)買號(hào)平臺(tái)更新引用以直接指向真正的資源。除了把操作的影響局部化以外,這種做法還有一個(gè)好處是,如果團(tuán)隊(duì)內(nèi)一個(gè)人在 move/rename 時(shí),另一個(gè)人創(chuàng)建了對(duì)老資源的引用,這個(gè)機(jī)制可以確保兩個(gè)人的工作被合并時(shí)能夠正常工作,而上面的“掃描并更新”的實(shí)踐則會(huì)導(dǎo)致后者的引用失效。
方式 III - 基于 GUID 的引用
(形如 '{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}')
?
使用 GUID 的優(yōu)點(diǎn)非常明顯——由于不依賴在磁盤上的具體位置,不管路徑和命名怎么變,只要 GUID 保持不變,就能保證總是索引到對(duì)應(yīng)的資源。
但問(wèn)題也非常明顯:
1.首先是可讀性問(wèn)題,給定任意一個(gè) GUID 必須依賴工具查找才知道對(duì)應(yīng)的資源是什么,對(duì)工作效率的影響是很大的。考慮到有時(shí)會(huì)無(wú)意中刪除或者忘了提交某個(gè)資源,僅憑一個(gè) GUID 沒(méi)有任何可能的途徑來(lái)知道缺失了什么,而如果是路徑的話我們至少有機(jī)會(huì)知道是哪個(gè)文件的問(wèn)題。(是的我們可以通過(guò)版本管理軟件來(lái) blame 可是如果該文件被多人修改過(guò)就很被動(dòng)了)
2.其次是額外信息的存儲(chǔ)和同步的問(wèn)題,由于很多文件格式本身是找不到位置存 GUID 的,這就需要單獨(dú)建一個(gè)同名的 .metadata 文件并與原文件一同管理,這進(jìn)一步增大了負(fù)擔(dān),降低了工作效率。更重的實(shí)踐使用一個(gè)中央數(shù)據(jù)庫(kù)來(lái)把所有資源的 GUID 收攏到一處統(tǒng)一管理,這就需要提供各種工具去處理更新,合并,與版本管理軟件協(xié)作等問(wèn)題。
確定性的 GUID 生成
由于工作關(guān)系,我曾在一個(gè)商業(yè)引擎的資源管理相關(guān)代碼上工作過(guò)一段時(shí)間。不幸的是,該引擎使用了 GUID 來(lái)管理資源的標(biāo)識(shí)和引用。更為不幸的是,該引擎通過(guò)“在打包時(shí)動(dòng)態(tài)地為資源生成 GUID ”來(lái)成功地把打包問(wèn)題和資源管理問(wèn)題深深地耦合在了一起。由于在開(kāi)發(fā)過(guò)程中,代碼和資源會(huì)持續(xù)地迭代變化,打包的環(huán)境總是處于或微小或劇烈的干擾之中,所有這些帶來(lái)的直接后果就是,打出的資源包內(nèi)大部分資源的 GUID 幾乎總是隨著版本在持續(xù)地變化,而前后兩次打包出的資源也無(wú)法兼容和重用。可以想見(jiàn),對(duì)于一個(gè)需要聯(lián)網(wǎng)并時(shí)常熱更新的游戲來(lái)說(shuō),這是一個(gè)多么不幸的設(shè)計(jì)。
為了解決這個(gè)問(wèn)題,經(jīng)過(guò)我跟另一位同事的先后努力,這個(gè)引擎中,涉及資源管理方面的所有的 GUID 生成都被我們改為了確定性的 (deterministic guid generation)。也就是盡量保證,在任何一個(gè)給定的上下文中,生成的 GUID 總是確定一致,并與該上下文基本對(duì)應(yīng)。這個(gè)確定性的 GUID 生成實(shí)踐,本質(zhì)上是一個(gè)通過(guò)使用互不干擾的多個(gè)隨機(jī)序列 (std::mt19937 & std::uniform_int_distribution ) ,抓取并嵌入上下文相關(guān)的信息,來(lái)把 GUID 的生成盡可能局部化的過(guò)程。關(guān)于此問(wèn)題的更詳細(xì)的記錄信息可參閱此文檔 (PDF),這里就不再細(xì)說(shuō)了。
經(jīng)過(guò)這次折騰,俺對(duì) GUID 用于折騰所能產(chǎn)生的巨大能量有了充分而深刻的認(rèn)識(shí)。此事的一個(gè)后遺癥是,從那以后聽(tīng)到用 GUID 管理引用和依賴的方案,俺就不由自主想呵呵了。
方式 IV - Unique Name 全局唯一命名
(形如 'v1_ui_mainframe_miracle_png_hd')
?
簡(jiǎn)單來(lái)說(shuō),Unique Name 本質(zhì)上是一個(gè)改良版的 (具有一定可讀性的) GUID。它兼具了路徑引用和 GUID 引用的優(yōu)點(diǎn) (可讀性好,可隨意修改物理路徑) 但除了改良的可讀性這一點(diǎn)之外,上面所有的 GUID 相關(guān)討論也同樣適用于 Unique Name。
當(dāng)資源量大到一定的體量并仍在持續(xù)增長(zhǎng)時(shí),(為了避免沖突) Unique Name 將變得越來(lái)越臃腫。過(guò)長(zhǎng)的描述不僅容易造成額外的管理和溝通負(fù)擔(dān),也會(huì)加大運(yùn)行時(shí)的內(nèi)存開(kāi)銷,實(shí)踐中在需要時(shí)可以 hash 一下。
改進(jìn)的實(shí)踐 - 路徑 + 摘要 (" Path + Digest ")
(形如 '/foo/bar/miracle.png: (digest-string)')
呼~(yú)~終于說(shuō)到這一次的實(shí)踐了。??
?
還好一句話就能說(shuō)清楚:在路徑后面加一個(gè)該資源的內(nèi)容摘要 (算法隨意不影響,目前使用 MD5) 就是我目前采取的方案。
關(guān)鍵點(diǎn)那么與上面的方案相比,這個(gè)方案有何不同呢?
1.資源重命名或移動(dòng)時(shí),能夠做到自動(dòng)檢測(cè)和修改。
- 一般情況下,如果僅僅是重命名或移動(dòng),根據(jù)內(nèi)容算出來(lái)的摘要是不變的,當(dāng)通過(guò)路徑找不到資源時(shí),通過(guò)比較摘要,就可以提示用戶 (或自動(dòng)重定向到) 重命名或移動(dòng)后的資源。
- 檢測(cè)和修改是可惰性的,可延遲至對(duì)應(yīng)的資源打開(kāi)時(shí)再轉(zhuǎn)換,不必立即一次性掃描和更新所有引用。
- 重命名和更新可以在 OS 的文件系統(tǒng)內(nèi)完成,無(wú)需在特定工具內(nèi)。
2.資源更新時(shí)自動(dòng)識(shí)別和更新摘要。
?
- 當(dāng)資源發(fā)生變化時(shí) (通常是美術(shù)/策劃保存了一個(gè)新版本) 編輯器會(huì)在加載此資源的引用者時(shí)為其生成新的摘要。
- 這個(gè)也是可惰性的,也就是加載了哪個(gè)資源,哪個(gè)資源才需要重新生成。
3.不像 GUID 那樣需要單獨(dú)存儲(chǔ),無(wú)需額外的 metadata 文件管理負(fù)擔(dān)。
?
- 由于摘要沒(méi)有產(chǎn)生資源以外的額外信息,隨時(shí)可以根據(jù)資源本身生成,所以無(wú)需額外的 metadata 文件。
4.簡(jiǎn)化全庫(kù)范圍的操作。
?
- 方便檢查重復(fù)資源 (全庫(kù)比較摘要即可);
- 全庫(kù)范圍自動(dòng)修復(fù)所有的重命名和移動(dòng) (完全應(yīng)用 1.);
- 全庫(kù)范圍自動(dòng)重算 (完全應(yīng)用 2.)。
實(shí)現(xiàn)邏輯
有同學(xué)可能會(huì)問(wèn):“如果移動(dòng),重命名,更新等各種操作混雜在一起,我怎么知道什么時(shí)候該自動(dòng)重定向,什么時(shí)候該更新摘要呢?”
嗯,這就是路徑 (Path) 和摘要結(jié)合 (Digest) 的精髓所在了。我們根據(jù)引用去查找資源時(shí),是按照下面?zhèn)未a的邏輯進(jìn)行的:
?
也就是說(shuō),路徑的判定優(yōu)先級(jí)高于摘要。在認(rèn)定屬于何種情況時(shí),路徑為主導(dǎo),摘要為輔助。如果路徑吻合但摘要不符,則認(rèn)為屬于資源更新的情況;如果路徑失效,則使用摘要去全庫(kù)匹配。兩種行為分別針對(duì)兩種不同情況的處理,涇渭分明,各司其職。
批量處置
上面的代碼是單個(gè)資源獲取的流程,實(shí)際上在編輯器中打開(kāi)一張地圖 (或一個(gè) UI 界面) 時(shí),如果一個(gè)資源一個(gè)資源地單獨(dú)匯報(bào)和處置,效率就太低了,可以在全部加載完畢后,統(tǒng)一批量地進(jìn)行一次全庫(kù)范圍的匹配,然后彈出一個(gè)匯報(bào)和處置的對(duì)話框。在這個(gè)處置對(duì)話框中,重命名/移動(dòng)/更新都是黃色嘆號(hào),而無(wú)法識(shí)別/找不到資源則是紅色嘆號(hào),通常如果都是黃色嘆號(hào)的話直接全部更新就可以了。
代碼中的引用
在代碼中為了簡(jiǎn)便,可以僅使用路徑即可。在運(yùn)行游戲的過(guò)程中,會(huì)自動(dòng)生成一個(gè) digest_cache.txt 文件,每一行是一個(gè)資源的完整引用,可以把這個(gè)文件提交到版本管理的庫(kù)中。這樣,很容易通過(guò)程序手段在資源發(fā)生重命名,移動(dòng)和更新等事件時(shí),檢測(cè)并更新這個(gè)文件,必要時(shí),可提示用戶代碼內(nèi)的路徑需要更新。
小結(jié)
總得來(lái)說(shuō),這個(gè)方案具有以下的特征:
?
- 良好的可讀性;
- 無(wú)需額外的 metadata 文件存儲(chǔ);
- 對(duì)資源的重命名/移動(dòng)無(wú)需在編輯器等專有工具內(nèi)完成,沒(méi)有潛在的破壞其他資源引用的心理負(fù)擔(dān);
- 唯一需要保證的是,重命名和移動(dòng)資源的時(shí)候,不要同時(shí)更新其內(nèi)容即可。
好了,關(guān)于這個(gè)資源引用管理的實(shí)踐,到這里就講完了。在資源管理方面,你有什么心得呢?歡迎跟我一起討論。
總結(jié)
以上是生活随笔為你收集整理的利用文件摘要简化游戏资源的引用管理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 游戏人工智能开发之6种决策方法
- 下一篇: 塔防游戏的路径寻找算法分析