日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

    
        
    歡迎訪問 生活随笔!

    生活随笔

    當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

    编程问答

    利用文件摘要简化游戏资源的引用管理

    發布時間:2024/8/26 编程问答 29 豆豆
    生活随笔 收集整理的這篇文章主要介紹了 利用文件摘要简化游戏资源的引用管理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

    資源的引用管理是個有趣的話題,最近我在代碼里實踐了一種做法,可以在某些方面簡化資源的管理,完成之后簡單記錄在這里。這篇文章先介紹傳統的各種方式,然后簡單說明一下,這個實踐在傳統方式的基礎上做了哪些改善,解決了什么問題。

    引子

    游戲開發中的資源管理,通常是指針對游戲中的各類資源數據 (模型,貼圖,腳本,數據表等等),通過合理安排布局來提高資源訪問的效率,進而改善游戲體驗的過程。在布局方面的一些實踐,譬如“如何區分對待不同的資源類型,如何做到更新友好”等等,這里就不詳細討論了。今天主要談一下在大量資源已合理布局的情況下,如何有效地處置它們相互之間巨量的依賴和引用關系的問題。

    簡單地說,如果 A 引用了 B,那么應該如何簡潔有效地表達這種引用呢?

    有經驗的開發者知道,這個問題并不像看上去這么簡單。隨著資源量的劇增,以及牽扯到的工作流程的細碎化,如果處置不善,資源引用問題會成為影響整個架構的根本性問題。

    傳統實踐

    方式 I - 基于偏移 (指針) 的引用

    文件偏移 (file-offset) 應該是最基本最原始的引用方式了。在一個運行著的 C/C++ 程序中,通常我們通過在對象 A 中存儲指針來引用對象 B。如果在序列化時,把這種指針引用以文件偏移的形式直接寫入文件,就是最原始的資源引用管理。
    ?


    這種最原始的依賴管理,細分一下還有兩種形式:

    1.每個對象的地址 (&object) 被一并存下來用作該對象的 ID (順便保證了全局唯一),將引用者寫入文件時,如果出現被引用者的指針,就直接寫入其地址。這么做的好處是簡單直接,速度快,與運行時地址空間一一對應,有時候甚至非常有利于調試。但缺點和限制是每個對象需要額外的4個字節 (64位就是8個字節),而且必須保證在序列化的過程中不發生相關內存的釋放和重新分配 (因為可能導致同一地址被不同的對象“復用”了)。

    2.每個對象在被寫入文件時,使用當時的文件偏移作為該對象的 ID (通過每個偏移在文件中的唯一性來保證全局唯一),將引用者寫入文件時,如果出現被引用者的指針,就寫入其文件偏移。這么做省去了指針的存儲開銷,但由于文件寫入是有先后次序的,先寫入的對象如果引用了后寫入的對象,此時還不知道文件偏移,就只有在第一遍寫完所有對象之后,再寫第二遍填上引用的空缺(或者是預先在內存中把偏移算好)。

    為什么說這種方案很原始呢,因為一個地址所能攜帶的信息太少了。在載入時,我們必須在整個過程中都非常清楚自己在操作什么類型的數據,這樣就需要大量額外的代碼來在不同的情況下創建不同類型的對象,這是非常繁瑣和易錯的。究其原因,就是引用的信息量不夠,做不到某種程度的自描述。

    關于打包的單獨討論

    由于這種方案足夠的快,在一些游戲引擎的二進制數據文件中有非常普遍的應用。為了保證讀取效率,游戲引擎通常會把邏輯上相關的資源打包在一起,避免反復讀取零散的文件。由于在包內的文件仍保持著與文件系統相一致的樹狀存儲結構,所以“物理包文件 + 虛擬的內部文件結構”,本質上跟典型的OS樹狀文件系統并無不同。提供這種打包機制的引擎通常會把這一層給抽象掉,大多數情況下,游戲代碼仍像訪問普通文件一樣去訪問內部的一個資源。這也就是在說,理想情況下,一個考慮周詳的打包機制,應做到保留 OS 文件系統的基本語意,將其自身透明化,不破壞和干擾已有的文件訪問方式。

    出于簡化討論的目的 (不影響討論的內容和結果),我們將只討論基于傳統的 OS 文件系統下的資源相互引用問題,而把“是否應該打包,如何打包”等問題正交地拆分出去,視作另一個維度的考慮。

    方式 II - 基于路徑的引用

    (形如 '/foo/bar/miracle.png')??
    ?

  1. texture = "/foo/bar/miracle.png";
  2. 復制代碼


    正如標題里的例子那樣,按照路徑來索引資源,應該是最自然和直觀的引用方式了。事實上,互聯網上的資源和服務,大部分都是通過 URL,以路徑方式來提供的。
    ?


    使用路徑來索引資源時,如有可能,應當盡量使用相同格式的歸一化的平臺無關的路徑?;煊?'\\' 和 '/',使用 "/../" 或 "/./",等等,都會造成無法直接比較兩個引用是否指向同一份資源,而且對同一資源的引用字符串 hash 的結果會不一致。

    當需要移動或重命名資源的時候,路徑就失效了。這時候,簡單的做法是,總是在編輯器提供的資源管理工具中進行 move/rename 的操作,這樣可以自動更新所有對該資源的引用。涉及到全庫范圍的掃描和修改,當資源量大時可能會非常慢。

    一個常見的實踐是使用所謂的 "Redirector",當 move/rename 發生時,在原來資源的位置放置一個跳轉,指向新的位置,這樣所有的相關資源都可以保持對原資源的引用,無需被動更新。在全庫范圍內,可以定期地運行自動化工具來清理這些跳轉,手機號買號平臺更新引用以直接指向真正的資源。除了把操作的影響局部化以外,這種做法還有一個好處是,如果團隊內一個人在 move/rename 時,另一個人創建了對老資源的引用,這個機制可以確保兩個人的工作被合并時能夠正常工作,而上面的“掃描并更新”的實踐則會導致后者的引用失效。


    方式 III - 基于 GUID 的引用

    (形如 '{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}')
    ?

  3. texture = "{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}";
  4. 復制代碼


    使用 GUID 的優點非常明顯——由于不依賴在磁盤上的具體位置,不管路徑和命名怎么變,只要 GUID 保持不變,就能保證總是索引到對應的資源。

    但問題也非常明顯:

    1.首先是可讀性問題,給定任意一個 GUID 必須依賴工具查找才知道對應的資源是什么,對工作效率的影響是很大的。考慮到有時會無意中刪除或者忘了提交某個資源,僅憑一個 GUID 沒有任何可能的途徑來知道缺失了什么,而如果是路徑的話我們至少有機會知道是哪個文件的問題。(是的我們可以通過版本管理軟件來 blame 可是如果該文件被多人修改過就很被動了)

    2.其次是額外信息的存儲和同步的問題,由于很多文件格式本身是找不到位置存 GUID 的,這就需要單獨建一個同名的 .metadata 文件并與原文件一同管理,這進一步增大了負擔,降低了工作效率。更重的實踐使用一個中央數據庫來把所有資源的 GUID 收攏到一處統一管理,這就需要提供各種工具去處理更新,合并,與版本管理軟件協作等問題。

    確定性的 GUID 生成

    由于工作關系,我曾在一個商業引擎的資源管理相關代碼上工作過一段時間。不幸的是,該引擎使用了 GUID 來管理資源的標識和引用。更為不幸的是,該引擎通過“在打包時動態地為資源生成 GUID ”來成功地把打包問題和資源管理問題深深地耦合在了一起。由于在開發過程中,代碼和資源會持續地迭代變化,打包的環境總是處于或微小或劇烈的干擾之中,所有這些帶來的直接后果就是,打出的資源包內大部分資源的 GUID 幾乎總是隨著版本在持續地變化,而前后兩次打包出的資源也無法兼容和重用??梢韵胍?#xff0c;對于一個需要聯網并時常熱更新的游戲來說,這是一個多么不幸的設計。

    為了解決這個問題,經過我跟另一位同事的先后努力,這個引擎中,涉及資源管理方面的所有的 GUID 生成都被我們改為了確定性的 (deterministic guid generation)。也就是盡量保證,在任何一個給定的上下文中,生成的 GUID 總是確定一致,并與該上下文基本對應。這個確定性的 GUID 生成實踐,本質上是一個通過使用互不干擾的多個隨機序列 (std::mt19937 & std::uniform_int_distribution ) ,抓取并嵌入上下文相關的信息,來把 GUID 的生成盡可能局部化的過程。關于此問題的更詳細的記錄信息可參閱此文檔 (PDF),這里就不再細說了。

    經過這次折騰,俺對 GUID 用于折騰所能產生的巨大能量有了充分而深刻的認識。此事的一個后遺癥是,從那以后聽到用 GUID 管理引用和依賴的方案,俺就不由自主想呵呵了。

    方式 IV - Unique Name 全局唯一命名

    (形如 'v1_ui_mainframe_miracle_png_hd')
    ?

  5. texture = "v1_ui_mainframe_miracle_png_hd";
  6. 復制代碼


    簡單來說,Unique Name 本質上是一個改良版的 (具有一定可讀性的) GUID。它兼具了路徑引用和 GUID 引用的優點 (可讀性好,可隨意修改物理路徑) 但除了改良的可讀性這一點之外,上面所有的 GUID 相關討論也同樣適用于 Unique Name。

    當資源量大到一定的體量并仍在持續增長時,(為了避免沖突) Unique Name 將變得越來越臃腫。過長的描述不僅容易造成額外的管理和溝通負擔,也會加大運行時的內存開銷,實踐中在需要時可以 hash 一下。

    改進的實踐 - 路徑 + 摘要 (" Path + Digest ")

    (形如 '/foo/bar/miracle.png: (digest-string)')

    呼~~終于說到這一次的實踐了。??
    ?

  7. texture = "/foo/bar/miracle.png:bd37de66ffdcfd5bf544502a1fae1e99";
  8. 復制代碼


    還好一句話就能說清楚:在路徑后面加一個該資源的內容摘要 (算法隨意不影響,目前使用 MD5) 就是我目前采取的方案。

    關鍵點那么與上面的方案相比,這個方案有何不同呢?

    1.資源重命名或移動時,能夠做到自動檢測和修改。

    • 一般情況下,如果僅僅是重命名或移動,根據內容算出來的摘要是不變的,當通過路徑找不到資源時,通過比較摘要,就可以提示用戶 (或自動重定向到) 重命名或移動后的資源。
    • 檢測和修改是可惰性的,可延遲至對應的資源打開時再轉換,不必立即一次性掃描和更新所有引用。
    • 重命名和更新可以在 OS 的文件系統內完成,無需在特定工具內。


    2.資源更新時自動識別和更新摘要。
    ?

    • 當資源發生變化時 (通常是美術/策劃保存了一個新版本) 編輯器會在加載此資源的引用者時為其生成新的摘要。
    • 這個也是可惰性的,也就是加載了哪個資源,哪個資源才需要重新生成。


    3.不像 GUID 那樣需要單獨存儲,無需額外的 metadata 文件管理負擔。
    ?

    • 由于摘要沒有產生資源以外的額外信息,隨時可以根據資源本身生成,所以無需額外的 metadata 文件。


    4.簡化全庫范圍的操作。
    ?

    • 方便檢查重復資源 (全庫比較摘要即可);
    • 全庫范圍自動修復所有的重命名和移動 (完全應用 1.);
    • 全庫范圍自動重算 (完全應用 2.)。


    實現邏輯

    有同學可能會問:“如果移動,重命名,更新等各種操作混雜在一起,我怎么知道什么時候該自動重定向,什么時候該更新摘要呢?”

    嗯,這就是路徑 (Path) 和摘要結合 (Digest) 的精髓所在了。我們根據引用去查找資源時,是按照下面偽碼的邏輯進行的:
    ?

  9. Resource* getResource(const std::string& refString)
  10. {
  11. ? ? // 分解為路徑和摘要兩部分
  12. ? ? std::string path = GetPathPart(refString);
  13. ? ? std::string digest = GetDigestPart(refString);
  14. ? ? // 嘗試訪問位于此路徑的文件
  15. ? ? Resource* res = GetActualFile(path);
  16. ? ? if (res)
  17. ? ? {
  18. ? ?? ???// 文件存在的情況,檢查摘要是否一致
  19. ? ?? ???if (digest == GetActualDigest(path))
  20. ? ?? ???{
  21. ? ?? ?? ?? ?return GetActualFile(path);
  22. ? ?? ???}
  23. ? ?? ???else
  24. ? ?? ???{
  25. ? ?? ?? ?? ?// 文件存在,摘要不一致,則認為是資源更新,重算摘要
  26. ? ?? ?? ?? ?RefreshDigest(path);
  27. ? ?? ???}
  28. ? ? }
  29. ? ? else
  30. ? ? {
  31. ? ?? ???// 文件如果不存在,符合重命名/移動的條件,提示用戶資源未找到,是否進行全庫范圍搜索
  32. ? ?? ???if (/*在另一個地點找到了摘要符合的資源*/)
  33. ? ?? ???{
  34. ? ?? ?? ?? ?// 提示用戶 (或自動) 更新引用路徑
  35. ? ?? ?? ?? ?RefreshPath(newPath);
  36. ? ?? ???}
  37. ? ?? ???else
  38. ? ?? ???{
  39. ? ?? ?? ?? ?// 提示資源缺失 (in-editor) 或使用 err-placeholder (in-runtime)
  40. ? ?? ?? ?? ?...? ?? ?? ?? ?
  41. ? ?? ???}? ?? ???
  42. ? ? }
  43. }
  44. 復制代碼


    也就是說,路徑的判定優先級高于摘要。在認定屬于何種情況時,路徑為主導,摘要為輔助。如果路徑吻合但摘要不符,則認為屬于資源更新的情況;如果路徑失效,則使用摘要去全庫匹配。兩種行為分別針對兩種不同情況的處理,涇渭分明,各司其職。

    批量處置

    上面的代碼是單個資源獲取的流程,實際上在編輯器中打開一張地圖 (或一個 UI 界面) 時,如果一個資源一個資源地單獨匯報和處置,效率就太低了,可以在全部加載完畢后,統一批量地進行一次全庫范圍的匹配,然后彈出一個匯報和處置的對話框。在這個處置對話框中,重命名/移動/更新都是黃色嘆號,而無法識別/找不到資源則是紅色嘆號,通常如果都是黃色嘆號的話直接全部更新就可以了。

    代碼中的引用

    在代碼中為了簡便,可以僅使用路徑即可。在運行游戲的過程中,會自動生成一個 digest_cache.txt 文件,每一行是一個資源的完整引用,可以把這個文件提交到版本管理的庫中。這樣,很容易通過程序手段在資源發生重命名,移動和更新等事件時,檢測并更新這個文件,必要時,可提示用戶代碼內的路徑需要更新。

    小結

    總得來說,這個方案具有以下的特征:
    ?

    • 良好的可讀性;
    • 無需額外的 metadata 文件存儲;
    • 對資源的重命名/移動無需在編輯器等專有工具內完成,沒有潛在的破壞其他資源引用的心理負擔;
    • 唯一需要保證的是,重命名和移動資源的時候,不要同時更新其內容即可。


    好了,關于這個資源引用管理的實踐,到這里就講完了。在資源管理方面,你有什么心得呢?歡迎跟我一起討論。

    總結

    以上是生活随笔為你收集整理的利用文件摘要简化游戏资源的引用管理的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。